<?php

/**
 *  RainTPL
 *  -------
 *  Realized by Federico Ulfo & maintained by the Rain Team
 *  Distributed under GNU/LGPL 3 License
 *
 *  @version 2.7.2
 */


class TPL{

    // -------------------------
    // 	CONFIGURATION
    // -------------------------

    /**
     * Template directory
     *
     * @var string
     */
    static $tpl_dir = "templates/";


    /**
     * Cache directory. Is the directory where RainTPL will compile the template and save the cache
     *
     * @var string
     */
    static $cache_dir = "cache/";


    /**
     * Template base URL. RainTPL will add this URL to the relative paths of element selected in $path_replace_list.
     *
     * @var string
     */
    static $base_url = null;


    /**
     * Template extension.
     *
     * @var string
     */
    static $tpl_ext = "html";


    /**
     * Path replace is a cool features that replace all relative paths of images (<img src="...">), stylesheet (<link href="...">), script (<script src="...">) and link (<a href="...">)
     * Set true to enable the path replace.
     *
     * @var unknown_type
     */
    static $path_replace = true;


    /**
     * You can set what the path_replace method will replace.
     * Avaible options: a, img, link, script, input
     *
     * @var array
     */
    static $path_replace_list = array( 'a', 'img', 'link', 'script', 'input' );


    /**
     * You can define in the black list what string are disabled into the template tags
     *
     * @var unknown_type
     */
    static $black_list = array( '\$this', 'raintpl::', 'self::', '_SESSION', '_SERVER', '_ENV',  'eval', 'exec', 'unlink', 'rmdir' );


    /**
     * Check template.
     * true: checks template update time, if changed it compile them
     * false: loads the compiled template. Set false if server doesn't have write permission for cache_directory.
     *
     */
    static $check_template_update = true;


    /**
     * PHP tags <? ?>
     * True: php tags are enabled into the template
     * False: php tags are disabled into the template and rendered as html
     *
     * @var bool
     */
    static $php_enabled = true;


    /**
     * Debug mode flag.
     * True: debug mode is used, syntax errors are displayed directly in template. Execution of script is not terminated.
     * False: exception is thrown on found error.
     *
     * @var bool
     */
    static $debug = true;

    // -------------------------


    // -------------------------
    // 	RAINTPL VARIABLES
    // -------------------------

    /**
     * Is the array where RainTPL keep the variables assigned
     *
     * @var array
     */
    public $var = array();

    protected $tpl = array(),		// variables to keep the template directories and info
        $cache = false,		// static cache enabled / disabled
        $cache_id = null;       // identify only one cache

    protected static $config_name_sum = array();   // takes all the config to create the md5 of the file

    // -------------------------



    const CACHE_EXPIRE_TIME = 3600; // default cache expire time = hour



    /**
     * Assign variable
     * eg. 	$t->assign('name','mickey');
     *
     * @param mixed $variable_name Name of template variable or associative array name/value
     * @param mixed $value value assigned to this variable. Not set if variable_name is an associative array
     */

    function assign( $variable, $value = null ){
        if( is_array( $variable ) )
            $this->var = $variable + $this->var;
        else
            $this->var[ $variable ] = $value;
    }



    /**
     * Draw the template
     * eg. 	$html = $tpl->draw( 'demo', TRUE ); // return template in string
     * or 	$tpl->draw( $tpl_name ); // echo the template
     *
     * @param string $tpl_name  template to load
     * @param boolean $return_string  true=return a string, false=echo the template
     * @return string
     */

    function draw( $tpl_name, $return_string = false ){

        try {
            // compile the template if necessary and set the template filepath
            $this->check_template( $tpl_name );
        } catch (RainTpl_Exception $e) {
            $output = $this->printDebug($e);
            die($output);
        }

        // Cache is off and, return_string is false
        // Rain just echo the template

        if( !$this->cache && !$return_string ){
            extract( $this->var );
            include $this->tpl['compiled_filename'];
            unset( $this->tpl );
        }


        // cache or return_string are enabled
        // rain get the output buffer to save the output in the cache or to return it as string

        else{

            //----------------------
            // get the output buffer
            //----------------------
            ob_start();
            extract( $this->var );
            include $this->tpl['compiled_filename'];
            $raintpl_contents = ob_get_clean();
            //----------------------


            // save the output in the cache
            if( $this->cache )
                file_put_contents( $this->tpl['cache_filename'], "<?php if(!class_exists('raintpl')){exit;}?>" . $raintpl_contents );

            // free memory
            unset( $this->tpl );

            // return or print the template
            if( $return_string ) return $raintpl_contents; else echo $raintpl_contents;

        }

    }

    /**
     * Draw the template
     */
    public function draw_string( $_string, $_to_string = false ){
        extract( $this->var );
        ob_start();

        require $this->_check_string( $_string );
        if( $_to_string ){
            $res =  ob_get_clean();
            return $res;
        }
        else{
            ob_get_clean();
        }

    }


    protected function _check_string( $string ){

        $template_name              = md5( $string);

        $parsed_template_filepath	= self::$cache_dir . $template_name . '.s.rtpl.php';
        $template_filepath          = '';
        $template_basedir			= '';


        // Compile the template if the original has been updated
        if( self::$debug ||  !file_exists( $parsed_template_filepath ) ){

            $this->_compile_string( $template_name, $template_basedir, $template_filepath, $parsed_template_filepath, $string );
        }
        return $parsed_template_filepath;
    }
    /**
     * Compile the file
     */

    protected function _compile_string(  $template_name, $template_basedir, $template_filepath, $parsed_template_filepath, $code ){

        // open the template
        $fp = fopen( $parsed_template_filepath, "w" );

        // lock the file
        if( flock( $fp, LOCK_SH ) ){

            // xml substitution
            $code = preg_replace( "/<\?xml(.*?)\?>/s", "##XML\\1XML##", $code );

            // disable php tag
            if( !self::$php_enabled )
                $code = str_replace( array("<?","?>"), array("&lt;?","?&gt;"), $code );

            /*
            // xml re-substitution
            $code = preg_replace_callback ( "/##XML(.*?)XML##/s", function( $match ){
                return "<?php echo '<?xml ".stripslashes($match[1])." ?>'; ?>";
            }, $code );
            */


            $parsed_code = $this->compileTemplate( $code,$template_basedir);
            $parsed_code = "<?php if(!class_exists('Tpl')){exit;}?>" . $parsed_code;


            // fix the php-eating-newline-after-closing-tag-problem
            $parsed_code = str_replace( "?>\n", "?>\n\n", $parsed_code );

            //error_log('parsed code :  ' . $code);


            // create directories
            if( !is_dir( self::$cache_dir ) )
                mkdir( self::$cache_dir, 0755, true );

            // check if the cache is writable
            if( !is_writable( self::$cache_dir ) ){
                error_log('chache dir not writable');
                throw new RainTpl_Exception ('Cache directory ' . self::$cache_dir . 'doesn\'t have write permission. Set write permission or set RAINTPL_CHECK_TEMPLATE_UPDATE to false. More details on http://www.raintpl.com/Documentation/Documentation-for-PHP-developers/Configuration/');
            }

            // write compiled file
            fwrite( $fp, $parsed_code );

            // release the file lock
            flock($fp, LOCK_UN);

        }

        // close the file
        fclose( $fp );

    }


    /**
     * If exists a valid cache for this template it returns the cache
     *
     * @param string $tpl_name Name of template (set the same of draw)
     * @param int $expiration_time Set after how many seconds the cache expire and must be regenerated
     * @return string it return the HTML or null if the cache must be recreated
     */

    function cache( $tpl_name, $expire_time = self::CACHE_EXPIRE_TIME, $cache_id = null ){

        // set the cache_id
        $this->cache_id = $cache_id;

        if( !$this->check_template( $tpl_name ) && file_exists( $this->tpl['cache_filename'] ) && ( time() - filemtime( $this->tpl['cache_filename'] ) < $expire_time ) ){
            // return the cached file as HTML. It remove the first 43 character, which are a PHP code to secure the file <?php if(!class_exists('raintpl')){exit;}? >
            return substr( file_get_contents( $this->tpl['cache_filename'] ), 43 );
        }
        else{
            //delete the cache of the selected template
            if (file_exists($this->tpl['cache_filename']))
                unlink($this->tpl['cache_filename'] );
            $this->cache = true;
        }
    }



    /**
     * Configure the settings of RainTPL
     *
     */
    static function configure( $setting, $value = null ){
        if( is_array( $setting ) )
            foreach( $setting as $key => $value )
                self::configure( $key, $value );
        else if( property_exists( __CLASS__, $setting ) ){
            self::$$setting = $value;
            self::$config_name_sum[ $setting ] = $value; // take trace of all config
        }
    }



    // check if has to compile the template
    // return true if the template has changed
    protected function check_template( $tpl_name ){

        if( !isset($this->tpl['checked']) ){

            $tpl_basename                       = basename( $tpl_name );														// template basename
            $tpl_basedir                        = strpos($tpl_name,"/") ? dirname($tpl_name) . '/' : null;						// template basedirectory
            $this->tpl['template_directory']    = self::$tpl_dir . $tpl_basedir;								// template directory
            $this->tpl['tpl_filename']          = $this->tpl['template_directory'] . $tpl_basename . '.' . self::$tpl_ext;	// template filename
            $temp_compiled_filename             = self::$cache_dir . $tpl_basename . "." . md5( $this->tpl['template_directory'] . serialize(self::$config_name_sum));
            $this->tpl['compiled_filename']     = $temp_compiled_filename . '.rtpl.php';	// cache filename
            $this->tpl['cache_filename']        = $temp_compiled_filename . '.s_' . $this->cache_id . '.rtpl.php';	// static cache filename
            $this->tpl['checked']               = true;

            // if the template doesn't exist and is not an external source throw an error
            if( self::$check_template_update && !file_exists( $this->tpl['tpl_filename'] ) && !preg_match('/http/', $tpl_name) ){
                $e = new RainTpl_NotFoundException( 'Template '. $tpl_basename .' not found!' );
                throw $e->setTemplateFile($this->tpl['tpl_filename']);
            }

            // We check if the template is not an external source
            if(preg_match('/http/', $tpl_name)){
                $this->compileFile('', '', $tpl_name, self::$cache_dir, $this->tpl['compiled_filename'] );
                return true;
            }
            // file doesn't exist, or the template was updated, Rain will compile the template
            elseif( !file_exists( $this->tpl['compiled_filename'] ) || ( self::$check_template_update && filemtime($this->tpl['compiled_filename']) < filemtime( $this->tpl['tpl_filename'] ) ) ){
                $this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], self::$cache_dir, $this->tpl['compiled_filename'] );
                return true;
            }

        }
    }


    /**
     * execute stripslaches() on the xml block. Invoqued by preg_replace_callback function below
     * @access protected
     */
    protected function xml_reSubstitution($capture) {
        return "<?php echo '<?xml ".stripslashes($capture[1])." ?>'; ?>";
    }

    /**
     * Compile and write the compiled template file
     * @access protected
     */
    protected function compileFile( $tpl_basename, $tpl_basedir, $tpl_filename, $cache_dir, $compiled_filename ){

        //read template file
        $this->tpl['source'] = $template_code = file_get_contents( $tpl_filename );

        //xml substitution
        $template_code = preg_replace( "/<\?xml(.*?)\?>/s", "##XML\\1XML##", $template_code );

        //disable php tag
        if( !self::$php_enabled )
            $template_code = str_replace( array("<?","?>"), array("&lt;?","?&gt;"), $template_code );

        //xml re-substitution
        $template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code );

        //compile template
        $template_compiled = "<?php if(!class_exists('raintpl')){exit;}?>" . $this->compileTemplate( $template_code, $tpl_basedir );


        // fix the php-eating-newline-after-closing-tag-problem
        $template_compiled = str_replace( "?>\n", "?>\n\n", $template_compiled );

        // create directories
        if( !is_dir( $cache_dir ) )
            mkdir( $cache_dir, 0755, true );

        if( !is_writable( $cache_dir ) )
            throw new RainTpl_Exception ('Cache directory ' . $cache_dir . 'doesn\'t have write permission. Set write permission or set RAINTPL_CHECK_TEMPLATE_UPDATE to false. More details on http://www.raintpl.com/Documentation/Documentation-for-PHP-developers/Configuration/');

        //write compiled file
        file_put_contents( $compiled_filename, $template_compiled );
    }



    /**
     * Compile template
     * @access protected
     */
    protected function compileTemplate( $template_code, $tpl_basedir ){

        //tag list
        $tag_regexp = array( 'loop'         => '(\{loop(?: name){0,1}="\${0,1}[^"]*"\})',
            'loop_close'   => '(\{\/loop\})',
            'if'           => '(\{if(?: condition){0,1}="[^"]*"\})',
            'elseif'       => '(\{elseif(?: condition){0,1}="[^"]*"\})',
            'else'         => '(\{else\})',
            'if_close'     => '(\{\/if\})',
            'function'     => '(\{function="[^"]*"\})',
            'noparse'      => '(\{noparse\})',
            'noparse_close'=> '(\{\/noparse\})',
            'ignore'       => '(\{ignore\}|\{\*)',
            'ignore_close'	=> '(\{\/ignore\}|\*\})',
            'include'      => '(\{include="[^"]*"(?: cache="[^"]*")?\})',
            'template_info'=> '(\{\$template_info\})',
            'function'		=> '(\{function="(\w*?)(?:.*?)"\})'
        );

        $tag_regexp = "/" . join( "|", $tag_regexp ) . "/";

        //split the code with the tags regexp
        $template_code = preg_split ( $tag_regexp, $template_code, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );

        //path replace (src of img, background and href of link)
        $template_code = $this->path_replace( $template_code, $tpl_basedir );

        //compile the code
        $compiled_code = $this->compileCode( $template_code );

        //return the compiled code
        return $compiled_code;

    }



    /**
     * Compile the code
     * @access protected
     */
    protected function compileCode( $parsed_code ){

        // if parsed code is empty return null string
        if( !$parsed_code )
            return "";

        //variables initialization
        $compiled_code = $open_if = $comment_is_open = $ignore_is_open = null;
        $loop_level = 0;


        //read all parsed code
        foreach( $parsed_code as $html ){

            //close ignore tag
            if( !$comment_is_open && ( strpos( $html, '{/ignore}' ) !== FALSE || strpos( $html, '*}' ) !== FALSE ) )
                $ignore_is_open = false;

            //code between tag ignore id deleted
            elseif( $ignore_is_open ){
                //ignore the code
            }

            //close no parse tag
            elseif( strpos( $html, '{/noparse}' ) !== FALSE )
                $comment_is_open = false;

            //code between tag noparse is not compiled
            elseif( $comment_is_open )
                $compiled_code .= $html;

            //ignore
            elseif( strpos( $html, '{ignore}' ) !== FALSE || strpos( $html, '{*' ) !== FALSE )
                $ignore_is_open = true;

            //noparse
            elseif( strpos( $html, '{noparse}' ) !== FALSE )
                $comment_is_open = true;

            //include tag
            elseif( preg_match( '/\{include="([^"]*)"(?: cache="([^"]*)"){0,1}\}/', $html, $code ) ){
                if (preg_match("/http/", $code[1])) {
                    $content = file_get_contents($code[1]);
                    $compiled_code .= $content;
                } else {
                    //variables substitution
                    $include_var = $this->var_replace( $code[ 1 ], $left_delimiter = null, $right_delimiter = null, $php_left_delimiter = '".' , $php_right_delimiter = '."', $loop_level );

                    //get the folder of the actual template
                    $actual_folder = substr( $this->tpl['template_directory'], strlen(self::$tpl_dir) );

                    //get the included template
                    $include_template = $actual_folder . $include_var;

                    // reduce the path
                    $include_template = $this->reduce_path( $include_template );

                    // if the cache is active
                    if( isset($code[ 2 ]) ){

                        //include
                        $compiled_code .= '<?php $tpl = new '.get_called_class().';' .
                            'if( $cache = $tpl->cache( "'.$include_template.'" ) )' .
                            '	echo $cache;' .
                            'else{' .
                            '$tpl->assign( $this->var );' .
                            ( !$loop_level ? null : '$tpl->assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ).
                            '$tpl->draw( "'.$include_template.'" );'.
                            '}' .
                            '?>';

                    }
                    else{
                        //include
                        $compiled_code .= '<?php $tpl = new '.get_called_class().';' .
                            '$tpl->assign( $this->var );' .
                            ( !$loop_level ? null : '$tpl->assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ).
                            '$tpl->draw( "'.$include_template.'" );'.
                            '?>';

                    }
                }
            }

            //loop
            elseif( preg_match( '/\{loop(?: name){0,1}="\${0,1}([^"]*)"\}/', $html, $code ) ){

                //increase the loop counter
                $loop_level++;

                //replace the variable in the loop
                $var = $this->var_replace( '$' . $code[ 1 ], $tag_left_delimiter=null, $tag_right_delimiter=null, $php_left_delimiter=null, $php_right_delimiter=null, $loop_level-1 );

                //loop variables
                $counter = "\$counter$loop_level";       // count iteration
                $key = "\$key$loop_level";               // key
                $value = "\$value$loop_level";           // value

                //loop code
                $compiled_code .=  "<?php $counter=-1; if( isset($var) && is_array($var) && sizeof($var) ) foreach( $var as $key => $value ){ $counter++; ?>";

            }

            //close loop tag
            elseif( strpos( $html, '{/loop}' ) !== FALSE ) {

                //iterator
                $counter = "\$counter$loop_level";

                //decrease the loop counter
                $loop_level--;

                //close loop code
                $compiled_code .=  "<?php } ?>";

            }

            //if
            elseif( preg_match( '/\{if(?: condition){0,1}="([^"]*)"\}/', $html, $code ) ){

                //increase open if counter (for intendation)
                $open_if++;

                //tag
                $tag = $code[ 0 ];

                //condition attribute
                $condition = $code[ 1 ];

                // check if there's any function disabled by black_list
                $this->function_check( $tag );

                //variable substitution into condition (no delimiter into the condition)
                $parsed_condition = $this->var_replace( $condition, $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );

                //if code
                $compiled_code .=   "<?php if( $parsed_condition ){ ?>";

            }

            //elseif
            elseif( preg_match( '/\{elseif(?: condition){0,1}="([^"]*)"\}/', $html, $code ) ){

                //tag
                $tag = $code[ 0 ];

                //condition attribute
                $condition = $code[ 1 ];

                //variable substitution into condition (no delimiter into the condition)
                $parsed_condition = $this->var_replace( $condition, $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );

                //elseif code
                $compiled_code .=   "<?php }elseif( $parsed_condition ){ ?>";
            }

            //else
            elseif( strpos( $html, '{else}' ) !== FALSE ) {

                //else code
                $compiled_code .=   '<?php }else{ ?>';

            }

            //close if tag
            elseif( strpos( $html, '{/if}' ) !== FALSE ) {

                //decrease if counter
                $open_if--;

                // close if code
                $compiled_code .=   '<?php } ?>';

            }

            //function
            elseif( preg_match( '/\{function="(\w*)(.*?)"\}/', $html, $code ) ){

                //tag
                $tag = $code[ 0 ];

                //function
                $function = $code[ 1 ];

                // check if there's any function disabled by black_list
                $this->function_check( $tag );

                if( empty( $code[ 2 ] ) )
                    $parsed_function = $function . "()";
                else
                    // parse the function
                    $parsed_function = $function . $this->var_replace( $code[ 2 ], $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );

                //if code
                $compiled_code .=   "<?php echo $parsed_function; ?>";
            }

            // show all vars
            elseif ( strpos( $html, '{$template_info}' ) !== FALSE ) {

                //tag
                $tag  = '{$template_info}';

                //if code
                $compiled_code .=   '<?php echo "<pre>"; print_r( $this->var ); echo "</pre>"; ?>';
            }


            //all html code
            else{

                //variables substitution (es. {$title})
                $html = $this->var_replace( $html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true );
                //const substitution (es. {#CONST#})
                $html = $this->const_replace( $html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true );
                //functions substitution (es. {"string"|functions})
                $compiled_code .= $this->func_replace( $html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true );
            }
        }

        if( $open_if > 0 ) {
            $e = new RainTpl_SyntaxException('Error! You need to close an {if} tag in ' . $this->tpl['tpl_filename'] . ' template');
            throw $e->setTemplateFile($this->tpl['tpl_filename']);
        }
        return $compiled_code;
    }


    /**
     * Reduce a path, eg. www/library/../filepath//file => www/filepath/file
     * @param type $path
     * @return type
     */
    protected function reduce_path( $path ){
        $path = str_replace( "://", "@not_replace@", $path );
        $path = preg_replace( "#(/+)#", "/", $path );
        $path = preg_replace( "#(/\./+)#", "/", $path );
        $path = str_replace( "@not_replace@", "://", $path );

        while( preg_match( '#\.\./#', $path ) ){
            $path = preg_replace('#\w+/\.\./#', '', $path );
        }
        return $path;
    }



    /**
     * replace the path of image src, link href and a href.
     * url => template_dir/url
     * url# => url
     * http://url => http://url
     *
     * @param string $html
     * @return string html sostituito
     */
    protected function path_replace( $html, $tpl_basedir ){

        if( self::$path_replace ){

            $tpl_dir = self::$base_url . self::$tpl_dir . $tpl_basedir;

            // reduce the path
            $path = $this->reduce_path($tpl_dir);

            $exp = $sub = array();

            if( in_array( "img", self::$path_replace_list ) ){
                $exp = array( '/<img(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<img(.*?)src=(?:")([^"]+?)#(?:")/i', '/<img(.*?)src="(.*?)"/', '/<img(.*?)src=(?:\@)([^"]+?)(?:\@)/i' );
                $sub = array( '<img$1src=@$2://$3@', '<img$1src=@$2@', '<img$1src="' . $path . '$2"', '<img$1src="$2"' );
            }

            if( in_array( "script", self::$path_replace_list ) ){
                $exp = array_merge( $exp , array( '/<script(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<script(.*?)src=(?:")([^"]+?)#(?:")/i', '/<script(.*?)src="(.*?)"/', '/<script(.*?)src=(?:\@)([^"]+?)(?:\@)/i' ) );
                $sub = array_merge( $sub , array( '<script$1src=@$2://$3@', '<script$1src=@$2@', '<script$1src="' . $path . '$2"', '<script$1src="$2"' ) );
            }

            if( in_array( "link", self::$path_replace_list ) ){
                $exp = array_merge( $exp , array( '/<link(.*?)href=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<link(.*?)href=(?:")([^"]+?)#(?:")/i', '/<link(.*?)href="(.*?)"/', '/<link(.*?)href=(?:\@)([^"]+?)(?:\@)/i' ) );
                $sub = array_merge( $sub , array( '<link$1href=@$2://$3@', '<link$1href=@$2@' , '<link$1href="' . $path . '$2"', '<link$1href="$2"' ) );
            }

            if( in_array( "a", self::$path_replace_list ) ){
                $exp = array_merge( $exp , array( '/<a(.*?)href=(?:")(http\:\/\/|https\:\/\/|javascript:|mailto:)([^"]+?)(?:")/i', '/<a(.*?)href="(.*?)"/', '/<a(.*?)href=(?:\@)([^"]+?)(?:\@)/i'  ) );
                $sub = array_merge( $sub , array( '<a$1href=@$2$3@', '<a$1href="' . self::$base_url . '$2"', '<a$1href="$2"' ) );
            }

            if( in_array( "input", self::$path_replace_list ) ){
                $exp = array_merge( $exp , array( '/<input(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<input(.*?)src=(?:")([^"]+?)#(?:")/i', '/<input(.*?)src="(.*?)"/', '/<input(.*?)src=(?:\@)([^"]+?)(?:\@)/i' ) );
                $sub = array_merge( $sub , array( '<input$1src=@$2://$3@', '<input$1src=@$2@', '<input$1src="' . $path . '$2"', '<input$1src="$2"' ) );
            }

            return preg_replace( $exp, $sub, $html );

        }
        else
            return $html;

    }





    // replace const
    function const_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null ){
        // const
        return preg_replace( '/\{\#(\w+)\#{0,1}\}/', $php_left_delimiter . ( $echo ? " echo " : null ) . '\\1' . $php_right_delimiter, $html );
    }



    // replace functions/modifiers on constants and strings
    function func_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null ){

        preg_match_all( '/' . '\{\#{0,1}(\"{0,1}.*?\"{0,1})(\|\w.*?)\#{0,1}\}' . '/', $html, $matches );

        for( $i=0, $n=count($matches[0]); $i<$n; $i++ ){

            //complete tag ex: {$news.title|substr:0,100}
            $tag = $matches[ 0 ][ $i ];

            //variable name ex: news.title
            $var = $matches[ 1 ][ $i ];

            //function and parameters associate to the variable ex: substr:0,100
            $extra_var = $matches[ 2 ][ $i ];

            // check if there's any function disabled by black_list
            $this->function_check( $tag );

            $extra_var = $this->var_replace( $extra_var, null, null, null, null, $loop_level );


            // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
            $is_init_variable = preg_match( "/^(\s*?)\=[^=](.*?)$/", $extra_var );

            //function associate to variable
            $function_var = ( $extra_var and $extra_var[0] == '|') ? substr( $extra_var, 1 ) : null;

            //variable path split array (ex. $news.title o $news[title]) or object (ex. $news->title)
            $temp = preg_split( "/\.|\[|\-\>/", $var );

            //variable name
            $var_name = $temp[ 0 ];

            //variable path
            $variable_path = substr( $var, strlen( $var_name ) );

            //parentesis transform [ e ] in [" e in "]
            $variable_path = str_replace( '[', '["', $variable_path );
            $variable_path = str_replace( ']', '"]', $variable_path );

            //transform .$variable in ["$variable"]
            $variable_path = preg_replace('/\.\$(\w+)/', '["$\\1"]', $variable_path );

            //transform [variable] in ["variable"]
            $variable_path = preg_replace('/\.(\w+)/', '["\\1"]', $variable_path );

            //if there's a function
            if( $function_var ){

                // check if there's a function or a static method and separate, function by parameters
                $function_var = str_replace("::", "@double_dot@", $function_var );

                // get the position of the first :
                if( $dot_position = strpos( $function_var, ":" ) ){

                    // get the function and the parameters
                    $function = substr( $function_var, 0, $dot_position );
                    $params = substr( $function_var, $dot_position+1 );

                }
                else{

                    //get the function
                    $function = str_replace( "@double_dot@", "::", $function_var );
                    $params = null;

                }

                // replace back the @double_dot@ with ::
                $function = str_replace( "@double_dot@", "::", $function );
                $params = str_replace( "@double_dot@", "::", $params );


            }
            else
                $function = $params = null;

            $php_var = $var_name . $variable_path;

            // compile the variable for php
            if( isset( $function ) ){
                if( $php_var )
                    $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
                else
                    $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $params ) )" : "$function()" ) . $php_right_delimiter;
            }
            else
                $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;

            $html = str_replace( $tag, $php_var, $html );

        }

        return $html;

    }



    function var_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null ){

        //all variables
        if( preg_match_all( '/' . $tag_left_delimiter . '\$(\w+(?:\.\${0,1}[A-Za-z0-9_]+)*(?:(?:\[\${0,1}[A-Za-z0-9_]+\])|(?:\-\>\${0,1}[A-Za-z0-9_]+))*)(.*?)' . $tag_right_delimiter . '/', $html, $matches ) ){

            for( $parsed=array(), $i=0, $n=count($matches[0]); $i<$n; $i++ )
                $parsed[$matches[0][$i]] = array('var'=>$matches[1][$i],'extra_var'=>$matches[2][$i]);

            foreach( $parsed as $tag => $array ){

                //variable name ex: news.title
                $var = $array['var'];

                //function and parameters associate to the variable ex: substr:0,100
                $extra_var = $array['extra_var'];

                // check if there's any function disabled by black_list
                $this->function_check( $tag );

                $extra_var = $this->var_replace( $extra_var, null, null, null, null, $loop_level );

                // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
                $is_init_variable = preg_match( "/^[a-z_A-Z\.\[\](\-\>)]*=[^=]*$/", $extra_var );

                //function associate to variable
                $function_var = ( $extra_var and $extra_var[0] == '|') ? substr( $extra_var, 1 ) : null;

                //variable path split array (ex. $news.title o $news[title]) or object (ex. $news->title)
                $temp = preg_split( "/\.|\[|\-\>/", $var );

                //variable name
                $var_name = $temp[ 0 ];

                //variable path
                $variable_path = substr( $var, strlen( $var_name ) );

                //parentesis transform [ e ] in [" e in "]
                $variable_path = str_replace( '[', '["', $variable_path );
                $variable_path = str_replace( ']', '"]', $variable_path );

                //transform .$variable in ["$variable"] and .variable in ["variable"]
                $variable_path = preg_replace('/\.(\${0,1}\w+)/', '["\\1"]', $variable_path );

                // if is an assignment also assign the variable to $this->var['value']
                if( $is_init_variable )
                    $extra_var = "=\$this->var['{$var_name}']{$variable_path}" . $extra_var;



                //if there's a function
                if( $function_var ){

                    // check if there's a function or a static method and separate, function by parameters
                    $function_var = str_replace("::", "@double_dot@", $function_var );


                    // get the position of the first :
                    if( $dot_position = strpos( $function_var, ":" ) ){

                        // get the function and the parameters
                        $function = substr( $function_var, 0, $dot_position );
                        $params = substr( $function_var, $dot_position+1 );

                    }
                    else{

                        //get the function
                        $function = str_replace( "@double_dot@", "::", $function_var );
                        $params = null;

                    }

                    // replace back the @double_dot@ with ::
                    $function = str_replace( "@double_dot@", "::", $function );
                    $params = str_replace( "@double_dot@", "::", $params );
                }
                else
                    $function = $params = null;

                //if it is inside a loop
                if( $loop_level ){
                    //verify the variable name
                    if( $var_name == 'key' )
                        $php_var = '$key' . $loop_level;
                    elseif( $var_name == 'value' )
                        $php_var = '$value' . $loop_level . $variable_path;
                    elseif( $var_name == 'counter' )
                        $php_var = '$counter' . $loop_level;
                    else
                        $php_var = '$' . $var_name . $variable_path;
                }else
                    $php_var = '$' . $var_name . $variable_path;

                // compile the variable for php
                if( isset( $function ) )
                    $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
                else
                    $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;

                $html = str_replace( $tag, $php_var, $html );


            }
        }

        return $html;
    }



    /**
     * Check if function is in black list (sandbox)
     *
     * @param string $code
     * @param string $tag
     */
    protected function function_check( $code ){

        $preg = '#(\W|\s)' . implode( '(\W|\s)|(\W|\s)', self::$black_list ) . '(\W|\s)#';

        // check if the function is in the black list (or not in white list)
        if( count(self::$black_list) && preg_match( $preg, $code, $match ) ){

            // find the line of the error
            $line = 0;
            $rows=explode("\n",$this->tpl['source']);
            while( !strpos($rows[$line],$code) )
                $line++;

            // stop the execution of the script
            $e = new RainTpl_SyntaxException('Unallowed syntax in ' . $this->tpl['tpl_filename'] . ' template');
            throw $e->setTemplateFile($this->tpl['tpl_filename'])
                ->setTag($code)
                ->setTemplateLine($line);
        }

    }

    /**
     * Prints debug info about exception or passes it further if debug is disabled.
     *
     * @param RainTpl_Exception $e
     * @return string
     */
    protected function printDebug(RainTpl_Exception $e){
        if (!self::$debug) {
            throw $e;
        }
        $output = sprintf('<h2>Exception: %s</h2><h3>%s</h3><p>template: %s</p>',
            get_class($e),
            $e->getMessage(),
            $e->getTemplateFile()
        );
        if ($e instanceof RainTpl_SyntaxException) {
            if (null != $e->getTemplateLine()) {
                $output .= '<p>line: ' . $e->getTemplateLine() . '</p>';
            }
            if (null != $e->getTag()) {
                $output .= '<p>in tag: ' . htmlspecialchars($e->getTag()) . '</p>';
            }
            if (null != $e->getTemplateLine() && null != $e->getTag()) {
                $rows=explode("\n",  htmlspecialchars($this->tpl['source']));
                $rows[$e->getTemplateLine()] = '<font color=red>' . $rows[$e->getTemplateLine()] . '</font>';
                $output .= '<h3>template code</h3>' . implode('<br />', $rows) . '</pre>';
            }
        }
        $output .= sprintf('<h3>trace</h3><p>In %s on line %d</p><pre>%s</pre>',
            $e->getFile(), $e->getLine(),
            nl2br(htmlspecialchars($e->getTraceAsString()))
        );
        return $output;
    }
}


/**
 * Basic Rain tpl exception.
 */
class RainTpl_Exception extends Exception{
    /**
     * Path of template file with error.
     */
    protected $templateFile = '';

    /**
     * Returns path of template file with error.
     *
     * @return string
     */
    public function getTemplateFile()
    {
        return $this->templateFile;
    }

    /**
     * Sets path of template file with error.
     *
     * @param string $templateFile
     * @return RainTpl_Exception
     */
    public function setTemplateFile($templateFile)
    {
        $this->templateFile = (string) $templateFile;
        return $this;
    }
}

/**
 * Exception thrown when template file does not exists.
 */
class RainTpl_NotFoundException extends RainTpl_Exception{
}

/**
 * Exception thrown when syntax error occurs.
 */
class RainTpl_SyntaxException extends RainTpl_Exception{
    /**
     * Line in template file where error has occured.
     *
     * @var int | null
     */
    protected $templateLine = null;

    /**
     * Tag which caused an error.
     *
     * @var string | null
     */
    protected $tag = null;

    /**
     * Returns line in template file where error has occured
     * or null if line is not defined.
     *
     * @return int | null
     */
    public function getTemplateLine()
    {
        return $this->templateLine;
    }

    /**
     * Sets  line in template file where error has occured.
     *
     * @param int $templateLine
     * @return RainTpl_SyntaxException
     */
    public function setTemplateLine($templateLine)
    {
        $this->templateLine = (int) $templateLine;
        return $this;
    }

    /**
     * Returns tag which caused an error.
     *
     * @return string
     */
    public function getTag()
    {
        return $this->tag;
    }

    /**
     * Sets tag which caused an error.
     *
     * @param string $tag
     * @return RainTpl_SyntaxException
     */
    public function setTag($tag)
    {
        $this->tag = (string) $tag;
        return $this;
    }
}

// -- end