diff --git a/classes/template/TemplateHandler.class.php b/classes/template/TemplateHandler.class.php index 034875e42..255ee702b 100644 --- a/classes/template/TemplateHandler.class.php +++ b/classes/template/TemplateHandler.class.php @@ -1,11 +1,13 @@ config = new \stdClass; + $this->config->version = 1; + $this->config->autoescape = false; + + // Set user information. + $this->user = Session::getMemberInfo() ?: new Helpers\SessionHelper(); + + // Cache commonly used configurations as static properties. + if (self::$_mtime === null) + { + self::$_mtime = filemtime(__FILE__); + } + if (self::$_delay_compile === null) + { + self::$_delay_compile = config('view.delay_compile') ?? 0; + } + + // If paths were provided, initialize immediately. + if ($dirname && $filename) + { + $this->_setSourcePath($dirname, $filename); + } + } + + /** + * Initialize and normalize paths. + * + * @param string $dirname + * @param string $filename + * @return void + */ + protected function _setSourcePath(string $dirname, string $filename): void + { + // Normalize the template path. Result will look like 'modules/foo/views/' + $dirname = trim(preg_replace('@^' . preg_quote(\RX_BASEDIR, '@') . '|\./@', '', strtr($dirname, ['\\' => '/', '//' => '/'])), '/') . '/'; + $dirname = preg_replace('/[\{\}\(\)\[\]<>\$\'"]/', '', $dirname); + $this->absolute_dirname = \RX_BASEDIR . $dirname; + $this->relative_dirname = $dirname; + + // Normalize the filename. Result will look like 'bar/example.html' + $filename = trim(strtr($filename, ['\\' => '/', '//' => '/']), '/'); + $filename = preg_replace('/[\{\}\(\)\[\]<>\$\'"]/', '', $filename); + if (!preg_match('/\.(html?|php)$/', $filename)) + { + $filename .= '.html'; + } + $this->filename = $filename; + $this->absolute_path = $this->absolute_dirname . $filename; + $this->relative_path = $this->relative_dirname . $filename; + $this->exists = Storage::exists($this->absolute_path); + $this->_setCachePath(); + } + + /** + * Set the path for the cache file. + * + * @return void + */ + protected function _setCachePath() + { + $this->cache_path = \RX_BASEDIR . 'files/cache/template/' . $this->relative_path . '.php'; + if ($this->exists) + { + Debug::addFilenameAlias($this->absolute_path, $this->cache_path); + } + } + + /** + * Compile and execute a template file. + * + * You don't need to pass any paths if you have already supplied them + * through the constructor. They exist for backward compatibility. + * + * $override_filename should be considered deprecated, as it is only + * used in faceOff (layout source editor). + * + * @param ?string $dirname + * @param ?string $filename + * @param ?string $override_filename + * @return string + */ + public function compile(?string $dirname = null, ?string $filename = null, ?string $override_filename = null) + { + // If paths are given, initialize now. + if ($dirname && $filename) + { + $this->_setSourcePath($dirname, $filename); + } + if ($override_filename) + { + $override_filename = trim(preg_replace('@^' . preg_quote(\RX_BASEDIR, '@') . '|\./@', '', strtr($override_filename, ['\\' => '/', '//' => '/'])), '/') . '/'; + $override_filename = preg_replace('/[\{\}\(\)\[\]<>\$\'"]/', '', $override_filename); + $this->absolute_path = \RX_BASEDIR . $override_filename; + $this->relative_path = $override_filename; + $this->exists = Storage::exists($this->absolute_path); + $this->_setCachePath(); + } + + // Return error if the source file does not exist. + if (!$this->exists) + { + $error_message = sprintf('Template not found: %s', $this->relative_path); + trigger_error($error_message, \E_USER_WARNING); + return escape($error_message); + } + + // Record the starting time. + $start = microtime(true); + + // Find the latest mtime of the source template and the template parser. + $filemtime = filemtime($this->absolute_path); + if ($filemtime > time() - self::$_delay_compile) + { + $latest_mtime = self::$_mtime; + } + else + { + $latest_mtime = max($filemtime, self::$_mtime); + } + + // If a cached result does not exist, or if it is stale, compile again. + if (!Storage::exists($this->cache_path) || filemtime($this->cache_path) < $latest_mtime) + { + $content = $this->_convert(); + if (!Storage::write($this->cache_path, $content)) + { + throw new Exception('Cannot write template cache file: ' . $this->cache_path); + } + } + + $output = $this->_execute(); + + // Record the time elapsed. + $elapsed_time = microtime(true) - $start; + if (!isset($GLOBALS['__template_elapsed__'])) + { + $GLOBALS['__template_elapsed__'] = 0; + } + $GLOBALS['__template_elapsed__'] += $elapsed_time; + + return $output; + } + + /** + * Compile a template and return the PHP code. + * + * @param string $dirname + * @param string $filename + * @return string + */ + public function compileDirect(string $dirname, string $filename): string + { + // Initialize paths. Return error if file does not exist. + $this->_setSourcePath($dirname, $filename); + if (!$this->exists) + { + $error_message = sprintf('Template not found: %s', $this->relative_path); + trigger_error($error_message, \E_USER_WARNING); + return escape($error_message); + } + + // Convert, but don't actually execute it. + return $this->_convert(); + } + + /** + * Convert template code to PHP using a version-specific parser. + * + * Directly passing $content as a string is not available as an + * official API. It only exists for unit testing. + * + * @return string + */ + protected function _convert(?string $content = null): string + { + // Read the source, or use the provided content. + if ($content === null && $this->exists) + { + $content = Storage::read($this->absolute_path); + $content = trim($content) . PHP_EOL; + } + if ($content === null || $content === '') + { + return ''; + } + + // Remove UTF-8 BOM and convert CRLF to LF. + $content = preg_replace(['/^\xEF\xBB\xBF/', '/\r\n/'], ['', "\n"], $content); + + // Check the config tag: or + $content = preg_replace_callback('!^!', function($match) { + $this->config->{$match[1]} = ($match[1] === 'version' ? intval($match[2]) : toBool($match[2])); + return sprintf('config->%s = %s; ?>', $match[1], var_export($this->config->{$match[1]}, true)); + }, $content); + + // Turn autoescape on if the version is 2 or greater. + if ($this->config->version >= 2) + { + $this->config->autoescape = true; + } + + // Call a version-specific parser to convert template code into PHP. + $class_name = '\Rhymix\Framework\Parsers\Template\TemplateParser_v' . $this->config->version; + $parser = new $class_name; + $content = $parser->convert($content, $this); + + return $content; + } + + /** + * Execute the converted template and return the output. + * + * @return string + */ + protected function _execute(): string + { + // Import Context and lang as local variables. + $__Context = \Context::getAll(); + global $lang; + + // Start the output buffer. + $this->ob_level = ob_get_level(); + ob_start(); + + // Include the compiled template. + include $this->cache_path; + + // Fetch the content of the output buffer until the buffer level is the same as before. + $content = ''; + while (ob_get_level() > $this->ob_level) + { + $content .= ob_get_clean(); + } + + // Insert comments for debugging. + if(Debug::isEnabledForCurrentUser() && \Context::getResponseMethod() === 'HTML' && !preg_match('/^<(?:\!DOCTYPE|\?xml)/', $content)) + { + $meta = '' . "\n"; + $content = sprintf($meta, 'Start') . $content . sprintf($meta, 'End'); + } + + return $content; + } +} diff --git a/common/framework/parsers/template/TemplateParser_v1.php b/common/framework/parsers/template/TemplateParser_v1.php new file mode 100644 index 000000000..409d977d3 --- /dev/null +++ b/common/framework/parsers/template/TemplateParser_v1.php @@ -0,0 +1,899 @@ +autoescape_config_exists = str_contains($content, '$this->config->autoescape = '); + $this->source_type = preg_match('!^((?:m\.)?[a-z]+)/!', $template->relative_dirname, $matches) ? $matches[1] : null; + $this->template = $template; + + // replace comments + $content = preg_replace('@@s', '', $content); + + // replace value of src in img/input/script tag + $content = preg_replace_callback('/<(?:img|input|script)(?:[^<>]*?)(?(?=cond=")(?:cond="[^"]+"[^<>]*)+|)[^<>]* src="(?!(?:https?|file|data):|[\/\{])([^"]+)"/is', array($this, '_replacePath'), $content); + + // replace value of srcset in img/source/link tag + $content = preg_replace_callback('/<(?:img|source|link)(?:[^<>]*?)(?(?=cond=")(?:cond="[^"]+"[^<>]*)+|)[^<>]* srcset="([^"]+)"/is', array($this, '_replaceSrcsetPath'), $content); + + // replace loop and cond template syntax + $content = $this->_parseInline($content); + + // include, unload/load, import + $content = preg_replace_callback('/{(@[\s\S]+?|(?=[\$\\\\]\w+|_{1,2}[A-Z]+|[!\(+-]|\w+(?:\(|::)|\d+|[\'"].*?[\'"]).+?)}|<(!--[#%])?(include|import|(un)?load(?(4)|(?:_js_plugin)?)|config)(?(2)\(["\']([^"\']+)["\'])(.*?)(?(2)\)--|\/)>|(\s*)/', array($this, '_parseResource'), $content); + + // remove block which is a virtual tag + $content = preg_replace('@@is', '', $content); + + // form auto generation + $temp = preg_replace_callback('/(|[^<>]+)*?>)(.*?)(<\/form>)/is', array($this, '_compileFormAuthGeneration'), $content); + if($temp) + { + $content = $temp; + } + + // prevent from calling directly before writing into file + $content = '' . $content; + + // restore curly braces from temporary entities + $content = self::_replaceTempEntities($content); + + // remove php script reopening + $content = preg_replace_callback('/([;{])?( )*\?\>\<\?php\s/', function($match) { + return $match[1] === '{' ? '{ ' : '; '; + }, $content); + + // remove empty lines + $content = preg_replace([ + '/>\<\?php } \?\>\n[\t\x20]*?(?=\n.*?|{[^}]*}|\"(?>'.*?'|.)*?\"|.)*?>)@s"; + $nodes = preg_split($split_regex, $content, -1, PREG_SPLIT_DELIM_CAPTURE); + + for($idx = 1, $node_len = count($nodes); $idx < $node_len; $idx+=2) + { + if(!($node = $nodes[$idx])) + { + continue; + } + + if(preg_match_all('@\s(loop|cond)="([^"]+)"@', $node, $matches)) + { + // this tag + $tag = substr($node, 1, strpos($node, ' ') - 1); + + // if the vale of $closing is 0, it means 'skipping' + $closing = 0; + + // process opening tag + foreach($matches[1] as $n => $stmt) + { + $expr = $matches[2][$n]; + $expr = self::_replaceVar($expr); + $closing++; + + switch($stmt) + { + case 'cond': + if (preg_match('/^\$[\\\\\w\[\]\'":>-]+$/i', $expr)) + { + $expr = "$expr ?? false"; + } + $nodes[$idx - 1] .= ""; + break; + case 'loop': + if(!preg_match('@^(?:(.+?)=>(.+?)(?:,(.+?))?|(.*?;.*?;.*?)|(.+?)\s*=\s*(.+?))$@', $expr, $expr_m)) + { + break; + } + if($expr_m[1]) + { + $expr_m[1] = trim($expr_m[1]); + $expr_m[2] = trim($expr_m[2]); + if(isset($expr_m[3]) && $expr_m[3]) + { + $expr_m[2] .= '=>' . trim($expr_m[3]); + } + $nodes[$idx - 1] .= sprintf('', $expr_m[1], $expr_m[2]); + } + elseif(isset($expr_m[4]) && $expr_m[4]) + { + $nodes[$idx - 1] .= ""; + } + elseif(isset($expr_m[5]) && $expr_m[5]) + { + $nodes[$idx - 1] .= ""; + } + break; + } + } + $node = preg_replace('@\s(loop|cond)="([^"]+)"@', '', $node); + + // find closing tag + $close_php = ''; + // self closing tag + if($node[1] == '!' || substr($node, -2, 1) == '/' || isset($self_closing[$tag])) + { + $nodes[$idx + 1] = $close_php . $nodes[$idx + 1]; + } + else + { + $depth = 1; + for($i = $idx + 2; $i < $node_len; $i+=2) + { + $nd = $nodes[$i]; + if(strpos($nd, $tag) === 1) + { + $depth++; + } + elseif(strpos($nd, '/' . $tag) === 1) + { + $depth--; + if(!$depth) + { + $nodes[$i - 1] .= $nodes[$i] . $close_php; + $nodes[$i] = ''; + break; + } + } + } + } + } + + if(strpos($node, '|cond="') !== false) + { + $node = preg_replace('@(\s[-\w:]+(?:="[^"]+?")?)\|cond="(.+?)"@s', '$1', $node); + $node = self::_replaceVar($node); + } + + if($nodes[$idx] != $node) + { + $nodes[$idx] = $node; + } + } + + $content = implode('', $nodes); + + return $content; + } + + /** + * preg_replace_callback handler + * replace php code. + * @param array $m + * @return string changed result + */ + private function _parseResource($m) + { + // {@ ... } or {$var} or {func(...)} + if($m[1]) + { + if(preg_match('@^(\w+)\(@', $m[1], $mm) && (!function_exists($mm[1]) && !in_array($mm[1], ['isset', 'unset', 'empty']))) + { + return $m[0]; + } + + if($m[1][0] == '@') + { + $m[1] = self::_replaceVar(substr($m[1], 1)); + return ""; + } + else + { + // Get escape options. + if($m[1] === '$content' && preg_match('@^layouts/.+/layout\.html$@', $this->template->relative_path)) + { + $escape_option = 'noescape'; + } + elseif(preg_match('/^\$(?:user_)?lang->[a-zA-Z0-9\_]+$/', $m[1])) + { + $escape_option = 'noescape'; + } + elseif(preg_match('/^lang\(.+\)$/', $m[1])) + { + $escape_option = 'noescape'; + } + else + { + $escape_option = $this->autoescape_config_exists ? 'auto' : 'noescape'; + } + + // Separate filters from variable. + if (preg_match('@^(.+?)(?_applyEscapeOption($var, $escape_option); + $var = "nl2br({$var})"; + $escape_option = 'noescape'; + break; + + case 'join': + $var = $filter_option ? "implode({$filter_option}, {$var})" : "implode(', ', {$var})"; + break; + + case 'date': + $var = $filter_option ? "getDisplayDateTime(ztime({$var}), {$filter_option})" : "getDisplayDateTime(ztime({$var}), 'Y-m-d H:i:s')"; + break; + + case 'format': + case 'number_format': + $var = $filter_option ? "number_format({$var}, {$filter_option})" : "number_format({$var})"; + break; + + case 'shorten': + case 'number_shorten': + $var = $filter_option ? "number_shorten({$var}, {$filter_option})" : "number_shorten({$var})"; + break; + + case 'link': + $var = $this->_applyEscapeOption($var, $escape_option); + if ($filter_option) + { + $filter_option = $this->_applyEscapeOption($filter_option, $escape_option); + $var = "'' . ($var) . ''"; + } + else + { + $var = "'' . ($var) . ''"; + } + $escape_option = 'noescape'; + break; + + default: + $filter = escape_sqstr($filter); + $var = "'INVALID FILTER ({$filter})'"; + } + } + + // Apply the escape option and return. + return '_applyEscapeOption($var, $escape_option) . ' ?>'; + } + } + + if($m[3]) + { + $attr = array(); + if($m[5]) + { + if(preg_match_all('@,(\w+)="([^"]+)"@', $m[6], $mm)) + { + foreach($mm[1] as $idx => $name) + { + $attr[$name] = $mm[2][$idx]; + } + } + $attr['target'] = $m[5]; + } + else + { + if(!preg_match_all('@ (\w+)="([^"]+)"@', $m[6], $mm)) + { + return $m[0]; + } + foreach($mm[1] as $idx => $name) + { + $attr[$name] = $mm[2][$idx]; + } + } + + switch($m[3]) + { + // or + case 'include': + if(!$this->template->relative_dirname || !$attr['target']) + { + return ''; + } + + if (preg_match('!^\\^/(.+)!', $attr['target'], $tmatches)) + { + $pathinfo = pathinfo(\RX_BASEDIR . $tmatches[1]); + $fileDir = $pathinfo['dirname']; + } + else + { + $pathinfo = pathinfo($attr['target']); + $fileDir = $this->_getRelativeDir($pathinfo['dirname']); + } + + if(!$fileDir) + { + return ''; + } + + return "compile('{$fileDir}','{$pathinfo['basename']}') ?>"; + // + case 'load_js_plugin': + $plugin = self::_replaceVar($m[5]); + $s = ""; + if(strpos($plugin, '$__Context') === false) + { + $plugin = "'{$plugin}'"; + } + + $s .= ""; + return $s; + // or or or + case 'import': + case 'load': + case 'unload': + $metafile = ''; + $metavars = ''; + $replacements = HTMLDisplayHandler::$replacements; + $attr['target'] = preg_replace(array_keys($replacements), array_values($replacements), $attr['target']); + $pathinfo = pathinfo($attr['target']); + $doUnload = ($m[3] === 'unload'); + $isRemote = !!preg_match('@^(https?:)?//@i', $attr['target']); + + if(!$isRemote) + { + if (preg_match('!^\\^/(.+)!', $attr['target'], $tmatches)) + { + $pathinfo = pathinfo($tmatches[1]); + $relativeDir = $pathinfo['dirname']; + $attr['target'] = $relativeDir . '/' . $pathinfo['basename']; + } + else + { + if(!preg_match('@^\.?/@', $attr['target'])) + { + $attr['target'] = './' . $attr['target']; + } + $relativeDir = $this->_getRelativeDir($pathinfo['dirname']); + $attr['target'] = $relativeDir . '/' . $pathinfo['basename']; + } + } + + switch($pathinfo['extension']) + { + case 'xml': + if($isRemote || $doUnload) + { + return ''; + } + // language file? + if($pathinfo['basename'] == 'lang.xml' || substr($pathinfo['dirname'], -5) == '/lang') + { + $result = "Context::loadLang('{$relativeDir}');"; + } + else + { + $result = "require_once('./classes/xml/XmlJsFilter.class.php');\$__xmlFilter=new XmlJsFilter('{$relativeDir}','{$pathinfo['basename']}');\$__xmlFilter->compile();"; + } + break; + case 'js': + if($doUnload) + { + $result = vsprintf("Context::unloadFile('%s', '');", [$attr['target'] ?? '']); + } + else + { + $metafile = isset($attr['target']) ? $attr['target'] : ''; + $result = vsprintf("Context::loadFile(['%s', '%s', '%s', '%s']);", [ + $attr['target'] ?? '', $attr['type'] ?? '', $isRemote ? $this->source_type : '', $attr['index'] ?? '', + ]); + } + break; + case 'css': + case 'less': + case 'scss': + if($doUnload) + { + $result = vsprintf("Context::unloadFile('%s', '', '%s');", [ + $attr['target'] ?? '', $attr['media'] ?? '', + ]); + } + else + { + $metafile = isset($attr['target']) ? $attr['target'] : ''; + $metavars = isset($attr['vars']) ? ($attr['vars'] ? self::_replaceVar($attr['vars']) : '') : ''; + $result = vsprintf("Context::loadFile(['%s', '%s', '%s', '%s', %s]);", [ + $attr['target'] ?? '', $attr['media'] ?? '', $isRemote ? $this->source_type : '', $attr['index'] ?? '', + isset($attr['vars']) ? ($attr['vars'] ? self::_replaceVar($attr['vars']) : '[]') : '[]', + ]); + } + break; + } + + $result = ""; + if($metafile) + { + if(!$metavars) + { + $result = "" . $result; + } + else + { + // LESS or SCSS needs the variables to be substituted. + $result = "" . $result; + } + } + + return $result; + // + case 'config': + $result = ''; + if(preg_match_all('@ (\w+)="([^"]+)"@', $m[6], $config_matches, PREG_SET_ORDER)) + { + foreach($config_matches as $config_match) + { + $config_value = toBool(trim(strtolower($config_match[2]))) ? 'true' : 'false'; + $result .= "\$this->config->{$config_match[1]} = $config_value;"; + } + } + return ""; + } + } + + // such as , , + if($m[7]) + { + $m[7] = substr($m[7], 1); + if(!$m[7]) + { + return '' . $m[9]; + } + if(!preg_match('/^(?:((?:end)?(?:if|switch|for(?:each)?|while)|end)|(else(?:if)?)|(break@)?(case|default)|(break))$/', $m[7], $mm)) + { + return ''; + } + if($mm[1]) + { + if($mm[1][0] == 'e') + { + return '' . $m[9]; + } + + $precheck = ''; + if($mm[1] == 'switch') + { + $m[9] = ''; + } + elseif($mm[1] == 'foreach') + { + $var = preg_replace('/^\s*\(\s*(.+?) .*$/', '$1', $m[8]); + $precheck = "if({$var})"; + } + return '' . $m[9]; + } + if($mm[2]) + { + return "" . $m[9]; + } + if($mm[4]) + { + return "" . $m[9]; + } + if($mm[5]) + { + return ""; + } + return ''; + } + return $m[0]; + } + + /** + * Apply escape option to an expression. + */ + private function _applyEscapeOption($str, $escape_option) + { + if (preg_match('/^\$[\\\\\w\[\]\'":>-]+$/i', $str)) + { + $str = preg_match('/^\$lang->/', $str) ? $str : "$str ?? ''"; + } + + switch($escape_option) + { + case 'escape': + return "htmlspecialchars({$str}, ENT_QUOTES, 'UTF-8', true)"; + case 'noescape': + return "{$str}"; + case 'autoescape': + return "htmlspecialchars({$str}, ENT_QUOTES, 'UTF-8', false)"; + case 'autolang': + return "(preg_match('/^\\$(?:user_)?lang->[a-zA-Z0-9\_]+$/', {$str}) ? ({$str}) : htmlspecialchars({$str}, ENT_QUOTES, 'UTF-8', false))"; + case 'auto': + default: + return "(\$this->config->autoescape ? htmlspecialchars({$str}, ENT_QUOTES, 'UTF-8', false) : ({$str}))"; + } + } + + /** + * change relative path + * @param string $path + * @return string + */ + private function _getRelativeDir($path) + { + $_path = $path; + + $fileDir = $this->template->absolute_dirname; + if($path[0] != '/') + { + $path = strtr(realpath($fileDir . '/' . $path), '\\', '/'); + } + + // for backward compatibility + if(!$path) + { + $dirs = explode('/', $fileDir); + $paths = explode('/', $_path); + $idx = array_search($paths[0], $dirs); + + if($idx !== false) + { + while($dirs[$idx] && $dirs[$idx] === $paths[0]) + { + array_splice($dirs, $idx, 1); + array_shift($paths); + } + $path = strtr(realpath($fileDir . '/' . implode('/', $paths)), '\\', '/'); + } + } + + $path = preg_replace('/^' . preg_quote(\RX_BASEDIR, '/') . '/', '', $path); + + return $path; + } + + /** + * Check if a string seems to contain a variable. + * + * @param string $str + * @return bool + */ + private static function _isVar($str) + { + return preg_match('@(?\$([a-z_][a-z0-9_]*)@i', function($matches) { + return '->' . self::_getTempEntityForChar('{') . '$__Context->' . $matches[1] . self::_getTempEntityForChar('}'); + }, $php); + + // Replace all other variables with Context attributes. + $php = preg_replace_callback('@(?|(?' . $matches[1]; + } + }, $php); + + return $php; + } + + /** + * Replace temporary entities to curly braces. + * + * @param string $str + * @return string + */ + private static function _replaceTempEntities($str) + { + return strtr($str, [ + '{' => '{', + '}' => '}', + ]); + } + + /** + * Get the temporary entity for a character. + * + * @param string $char + * @return string + */ + private static function _getTempEntityForChar($char) + { + return '&#x' . strtoupper(bin2hex($char)) . ';'; + } +} diff --git a/common/framework/parsers/template/TemplateParser_v2.php b/common/framework/parsers/template/TemplateParser_v2.php new file mode 100644 index 000000000..a236a1c8b --- /dev/null +++ b/common/framework/parsers/template/TemplateParser_v2.php @@ -0,0 +1,37 @@ +template = $template; + + // Convert echo statements. + $content = preg_replace('!\{([^{}]+)\}!', '', $content); + + // Remove spaces before and after all PHP tags, in order to maintain clean alignment. + $content = preg_replace([ + '!(?<=^|\n)([\x20\x09]+)(<\?(?:php\b|=))!', + '!(\?>)([\x20\x09]+)(?=$|\r|\n)!', + ], ['$2', '$1'], $content); + + return $content; + } +} diff --git a/tests/unit/classes/TemplateHandlerTest.php b/tests/unit/classes/TemplateHandlerTest.php index 399ca5518..c0d0151a2 100644 --- a/tests/unit/classes/TemplateHandlerTest.php +++ b/tests/unit/classes/TemplateHandlerTest.php @@ -3,7 +3,7 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test { private $baseurl; - private $prefix = 'inst, 'init'), $tpl_path, $tpl_filename, $tpl_file); + $this->inst->_setSourcePath($tpl_path, $tpl_filename, $tpl_file); } public function parse($buff = null) { - return call_user_func(array($this->inst, 'parse'), $buff); + return $this->inst->_convert($buff); } }