Merge pull request #2200 from kijin/template-v2

템플릿 언어 v2
This commit is contained in:
Kijin Sung 2023-10-23 20:16:04 +09:00 committed by GitHub
commit 1d079e3194
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 4859 additions and 1272 deletions

View file

@ -2,7 +2,7 @@ RewriteEngine On
# block direct access to templates, XML schema files, config files, dotfiles, environment, etc.
RewriteCond %{REQUEST_URI} !/modules/editor/(skins|styles)/
RewriteRule ^(addons|common/tpl|files/ruleset|(m\.)?layouts|modules|plugins|themes|widgets|widgetstyles)/.+\.(html|xml)$ - [L,F]
RewriteRule ^(addons|common/tpl|files/ruleset|(m\.)?layouts|modules|plugins|themes|widgets|widgetstyles)/.+\.(html|xml|blade\.php)$ - [L,F]
RewriteRule ^files/(attach|config|cache)/.+\.(ph(p|t|ar)?[0-9]?|p?html?|cgi|pl|exe|[aj]spx?|inc|bak)$ - [L,F]
RewriteRule ^files/(env|member_extra_info/(new_message_flags|point))/ - [L,F]
RewriteRule ^(\.git|\.ht|\.travis|codeception\.|composer\.|Gruntfile\.js|package\.json|CONTRIBUTING|COPYRIGHT|LICENSE|README) - [L,F]

View file

@ -49,13 +49,12 @@ class HTMLDisplayHandler
*/
public function toDoc(&$oModule)
{
$oTemplate = TemplateHandler::getInstance();
// SECISSUE https://github.com/xpressengine/xe-core/issues/1583
$oSecurity = new Security();
$oSecurity->encodeHTML('is_keyword', 'search_keyword', 'search_target', 'order_target', 'order_type');
$template_path = $oModule->getTemplatePath();
$template_file = $oModule->getTemplateFile();
if(!is_dir($template_path))
{
@ -95,8 +94,8 @@ class HTMLDisplayHandler
}
}
$tpl_file = $oModule->getTemplateFile();
$output = $oTemplate->compile($template_path, $tpl_file);
$oTemplate = new Rhymix\Framework\Template($template_path, $template_file);
$output = $oTemplate->compile();
// add .x div for adminitration pages
if(Context::getResponseMethod() == 'HTML')
@ -161,6 +160,8 @@ class HTMLDisplayHandler
{
$layout_file = 'default_layout';
}
$oTemplate = new Rhymix\Framework\Template;
$output = $oTemplate->compile($layout_path, $layout_file, $edited_layout_file);
// if popup_layout, remove admin bar.
@ -291,7 +292,6 @@ class HTMLDisplayHandler
// convert the final layout
Context::set('content', $output);
$oTemplate = TemplateHandler::getInstance();
if(Mobile::isFromMobilePhone())
{
$this->_loadMobileJSCSS();
@ -300,7 +300,8 @@ class HTMLDisplayHandler
{
$this->_loadDesktopJSCSS();
}
$output = $oTemplate->compile('./common/tpl', 'common_layout');
$oTemplate = new Rhymix\Framework\Template('./common/tpl', 'common_layout');
$output = $oTemplate->compile();
// replace the user-defined-language
$output = Context::replaceUserLang($output);

View file

@ -8,8 +8,8 @@ class RawDisplayHandler
$tpl_file = $oModule->getTemplateFile();
if ($tpl_path && $tpl_file)
{
$oTemplate = TemplateHandler::getInstance();
$output = $oTemplate->compile($tpl_path, $tpl_file);
$oTemplate = new Rhymix\Framework\Template($tpl_path, $tpl_file);
$output = $oTemplate->compile();
}
else
{

View file

@ -218,8 +218,8 @@ class ModuleObject extends BaseObject
}
else
{
$oTemplate = TemplateHandler::getInstance();
$oTemplate->compile('modules/admin/tpl', '_admin_common.html');
$oTemplate = new Rhymix\Framework\Template('modules/admin/tpl', '_admin_common.html');
$oTemplate->compile();
}
}
@ -465,10 +465,12 @@ class ModuleObject extends BaseObject
*/
public function setTemplateFile($filename)
{
/*
if(isset($filename) && substr_compare($filename, '.html', -5) !== 0)
{
$filename .= '.html';
}
*/
$this->template_file = $filename;
return $this;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,919 @@
<?php
namespace Rhymix\Framework;
/**
* The template class.
*/
class Template
{
/**
* Properties for user
*/
public $user;
/**
* Properties for configuration
*/
public $config;
public $source_type;
public $parent;
public $vars;
/**
* Properties for path manipulation
*/
public $absolute_dirname;
public $relative_dirname;
public $filename;
public $extension;
public $exists;
public $absolute_path;
public $relative_path;
/**
* Properties for caching
*/
public $cache_enabled = true;
public $cache_path;
/**
* Properties for state management during compilation/execution
*/
protected $_ob_level;
protected $_fragments = [];
protected static $_loopvars = [];
protected static $_stacks = [];
/**
* Properties for optimization
*/
protected static $_mtime;
protected static $_delay_compile;
protected static $_json_options;
/**
* Provided for compatibility with old TemplateHandler.
*
* @return self
*/
public static function getInstance(): self
{
return new self();
}
/**
* You can also call the constructor directly.
*
* @param ?string $dirname
* @param ?string $filename
* @param ?string $extension
* @return void
*/
public function __construct(?string $dirname = null, ?string $filename = null, ?string $extension = null)
{
// Initialize configuration.
$this->_initConfig();
// Set user information.
$this->user = Session::getMemberInfo() ?: new Helpers\SessionHelper();
// Populate static properties for optimization.
if (self::$_mtime === null)
{
self::$_mtime = filemtime(__FILE__);
}
if (self::$_delay_compile === null)
{
self::$_delay_compile = config('view.delay_compile') ?? 0;
}
if (self::$_json_options === null)
{
self::$_json_options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_APOS | \JSON_HEX_QUOT | \JSON_UNESCAPED_UNICODE;
}
// If paths were provided, initialize immediately.
if ($dirname && $filename)
{
$this->_setSourcePath($dirname, $filename, $extension ?? 'auto');
}
}
/**
* Initialize the configuration object.
*
* @return void
*/
protected function _initConfig(): void
{
$this->config = new \stdClass;
$this->config->version = 1;
$this->config->autoescape = false;
$this->config->context = 'HTML';
}
/**
* Initialize and normalize paths.
*
* @param string $dirname
* @param string $filename
* @param string $extension
* @return void
*/
protected function _setSourcePath(string $dirname, string $filename, string $extension = 'auto'): 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 the filename doesn't have a typical extension and doesn't exist, try adding common extensions.
if (!preg_match('/\.(?:html?|php)$/', $filename) && !Storage::exists($this->absolute_dirname . $filename))
{
if ($extension !== 'auto')
{
$filename .= '.' . $extension;
$this->extension = $extension;
}
elseif (Storage::exists($this->absolute_dirname . $filename . '.html'))
{
$filename .= '.html';
$this->extension = 'html';
$this->exists = true;
}
elseif (Storage::exists($this->absolute_dirname . $filename . '.blade.php'))
{
$filename .= '.blade.php';
$this->extension = 'blade.php';
$this->exists = true;
}
else
{
$filename .= '.html';
$this->extension = 'html';
}
}
// Set the remainder of properties.
$this->filename = $filename;
$this->absolute_path = $this->absolute_dirname . $filename;
$this->relative_path = $this->relative_dirname . $filename;
if ($this->extension === null)
{
$this->extension = preg_match('/\.(blade\.php|[a-z]+)$/i', $filename, $m) ? $m[1] : '';
}
if ($this->exists === null)
{
$this->exists = Storage::exists($this->absolute_path);
}
if ($this->exists && $this->extension === 'blade.php')
{
$this->config->version = 2;
$this->config->autoescape = true;
}
$this->source_type = preg_match('!^((?:m\.)?[a-z]+)/!', $this->relative_dirname, $match) ? $match[1] : null;
$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 . '.compiled.php';
if ($this->exists)
{
Debug::addFilenameAlias($this->absolute_path, $this->cache_path);
}
}
/**
* Disable caching.
*
* @return void
*/
public function disableCache(): void
{
$this->cache_enabled = false;
}
/**
* Check if the template file exists.
*
* @return bool
*/
public function exists(): bool
{
return $this->exists ? true : false;
}
/**
* Get the parent template.
*
* @return ?self
*/
public function getParent(): ?self
{
return $this->parent;
}
/**
* Set the parent template.
*
* @param ?self $parent
* @return void
*/
public function setParent(self $parent): void
{
$this->parent = $parent;
}
/**
* Get vars.
*
* @return ?object
*/
public function getVars(): ?object
{
return $this->vars;
}
/**
* Set vars.
*
* @param array|object $vars
* @return void
*/
public function setVars($vars): void
{
if (is_array($vars))
{
$this->vars = (object)$vars;
}
elseif (is_object($vars))
{
$this->vars = clone $vars;
}
else
{
throw new Exception('Template vars must be an array or object');
}
}
/**
* Add vars.
*
* @param array|object $vars
* @return void
*/
public function addVars($vars): void
{
if (!isset($this->vars))
{
$this->vars = new \stdClass;
}
foreach (is_object($vars) ? get_object_vars($vars) : $vars as $key => $val)
{
$this->vars->$key = $val;
}
}
/**
* 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->_initConfig();
$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 || !$this->cache_enabled)
{
$content = $this->parse();
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->_initConfig();
$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);
}
// Parse the template, but don't actually execute it.
return $this->parse();
}
/**
* 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
*/
public function parse(?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 === '' || $content === PHP_EOL)
{
return '';
}
// Remove UTF-8 BOM and convert CRLF to LF.
$content = preg_replace(['/^\xEF\xBB\xBF/', '/\r\n/'], ['', "\n"], $content);
// Check the config tag: <config version="2" /> or <config autoescape="on" />
$content = preg_replace_callback('!(?<=^|\n)<config\s+(\w+)="([^"]+)"\s*/?>!', function($match) {
$this->config->{$match[1]} = ($match[1] === 'version' ? intval($match[2]) : toBool($match[2]));
return sprintf('<?php $this->config->%s = %s; ?>', $match[1], var_export($this->config->{$match[1]}, true));
}, $content);
// Check the alternative version directive: @version(2)
$content = preg_replace_callback('!(?<=^|\n)@version\s?\(([0-9]+)\)!', function($match) {
$this->config->version = intval($match[1]);
return sprintf('<?php $this->config->version = %s; ?>', var_export($this->config->version, true));
}, $content);
// 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
*/
public function execute(): string
{
// Import Context and lang as local variables.
$__Context = $this->vars ?: \Context::getAll();
// 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 = '<!--#Template%s:' . $this->relative_path . '-->' . "\n";
$content = sprintf($meta, 'Start') . $content . sprintf($meta, 'End');
}
return $content;
}
/**
* Get a fragment of the executed output.
*
* @param string $name
* @return ?string
*/
public function getFragment(string $name): ?string
{
if (isset($this->_fragments[$name]))
{
return $this->_fragments[$name];
}
else
{
return null;
}
}
/**
* Get the contents of a stack.
*
* @param string $name
* @return ?array
*/
public function getStack(string $name): ?array
{
if (isset(self::$_stacks[$name]))
{
return self::$_stacks[$name];
}
else
{
return null;
}
}
/**
* Check if a path should be treated as relative to the path of the current template.
*
* @param string $path
* @return bool
*/
public function isRelativePath(string $path): bool
{
return !preg_match('#^((?:https?|file|data):|[\/\{<])#i', $path);
}
/**
* Convert a relative path using the given basepath.
*
* @param string $path
* @param ?string $basepath
* @return string
*/
public function convertPath(string $path, ?string $basepath = null): string
{
// If basepath is not provided, use the relative dir of the current instance.
if ($basepath === null)
{
$basepath = $this->relative_dirname;
}
// Path relative to the Rhymix installation directory?
if (preg_match('#^\^/?(\w.+)$#s', $path, $match))
{
$path = \RX_BASEURL . $match[1];
}
// Other paths will be relative to the given basepath.
else
{
$path = preg_replace('#/\./#', '/', $basepath . $path);
}
// Normalize and return the path.
return $this->normalizePath($path);
}
/**
* Normalize a path by removing extra slashes and parent directory references.
*
* @param string $path
* @return string
*/
public function normalizePath(string $path): string
{
$path = preg_replace('#[\\\\/]+#', '/', $path);
while (($tmp = preg_replace('#/[^/]+/\.\.(/|$)#', '$1', $path)) !== $path)
{
$path = $tmp;
}
return $path;
}
/**
* =================== HELPER FUNCTIONS FOR TEMPLATE v2 ===================
*/
/**
* Include another template from v2 @include directive.
*
* Blade has several variations of the @include directive, and we need
* access to the actual PHP args in order to process them accurately.
* So we do this in the Template class, not in the converter.
*
* @param ...$args
* @return string
*/
protected function _v2_include(...$args): string
{
// Set some basic information.
$directive = $args[0];
$extension = $this->extension === 'blade.php' ? 'blade.php' : null;
$isConditional = in_array($directive, ['includeWhen', 'includeUnless']);
$basedir = $this->relative_dirname;
$cond = $isConditional ? $args[1] : null;
$path = $isConditional ? $args[2] : $args[1];
$vars = $isConditional ? ($args[3] ?? null) : ($args[2] ?? null);
// If the conditions are not met, return.
if ($isConditional && $directive === 'includeWhen' && !$cond)
{
return '';
}
if ($isConditional && $directive === 'includeUnless' && $cond)
{
return '';
}
// Handle paths relative to the Rhymix installation directory.
if (preg_match('#^\^/?(\w.+)$#s', $path, $match))
{
$basedir = str_contains($match[1], '/') ? dirname($match[1]) : \RX_BASEDIR;
$path = basename($match[1]);
}
// Convert relative paths embedded in the filename.
if (preg_match('#^(.+)/([^/]+)$#', $path, $match))
{
$basedir = $this->normalizePath($basedir . $match[1] . '/');
$path = $match[2];
}
// Create a new instance of TemplateHandler.
$template = new self($basedir, $path, $extension);
// If the directive is @includeIf and the template file does not exist, return.
if ($directive === 'includeIf' && !$template->exists())
{
return '';
}
// Set variables.
$template->setParent($this);
if ($this->vars)
{
$template->setVars($this->vars);
}
if ($vars !== null)
{
$template->addVars($vars);
}
// Compile and return.
return $template->compile();
}
/**
* Load a resource from v2 @load directive.
*
* The Blade-style syntax does not have named arguments, so we must rely
* on the position and format of each argument to guess what it is for.
* Fortunately, there are only a handful of valid options for the type,
* media, and index attributes.
*
* @param string $path
* @param string $media_type
* @param int $index
* @param array|object $vars
* @return void
*/
protected function _v2_loadResource(string $path, $media_type = null, $index = null, $vars = null): void
{
// Assign the path.
if (empty($path))
{
trigger_error('Resource loading directive used with no path', \E_USER_WARNING);
return;
}
// Check whether the path is an internal or external link.
$external = false;
if (preg_match('#^\^#', $path))
{
$path = './' . ltrim($path, '^/');
}
elseif ($this->isRelativePath($path))
{
$path = $this->convertPath($path, './' . $this->relative_dirname);
}
else
{
$external = true;
}
// If any of the variables seems to be an array or object, it's $vars.
if (!is_scalar($media_type))
{
$vars = $media_type;
$media_type = null;
}
if (!is_scalar($index))
{
$vars = $index;
$index = null;
}
if (ctype_digit($media_type ?? ''))
{
$index = $media_type;
$media_type = null;
}
// Split the media type if it has a colon in it.
if (preg_match('#^(css|js):(.+)$#s', $media_type ?? '', $match))
{
$media_type = trim($match[2]);
$type = $match[1];
}
// Determine the type of resource.
elseif (!$external && str_starts_with($path, './common/js/plugins/'))
{
$type = 'jsplugin';
}
elseif (!$external && preg_match('#/lang(\.xml)?$#', $path))
{
$type = 'lang';
}
elseif (preg_match('#\.(css|js|scss|less)($|\?|/)#', $path, $match))
{
$type = $match[1];
}
elseif (preg_match('#/css\d?\?.+#', $path))
{
$type = 'css';
}
else
{
$type = 'unknown';
}
// Load the resource.
if ($type === 'jsplugin')
{
if (preg_match('#/common/js/plugins/([^/]+)#', $path, $match))
{
$plugin_name = $match[1];
\Context::loadJavascriptPlugin($plugin_name);
}
else
{
trigger_error("Unable to find JS plugin at $path", \E_USER_WARNING);
}
}
elseif ($type === 'lang')
{
$lang_dir = preg_replace('#/lang\.xml$#', '', $path);
\Context::loadLang($lang_dir);
}
elseif ($type === 'js')
{
\Context::loadFile([
$path,
$media_type ?? '',
$external ? $this->source_type : '',
$index ? intval($index) : '',
]);
}
elseif ($type === 'css' || $type === 'scss' || $type === 'less')
{
\Context::loadFile([
$path,
$media_type ?? '',
$external ? $this->source_type : '',
$index ? intval($index) : '',
$vars ?? [],
]);
}
else
{
trigger_error("Unable to determine type of resource at $path", \E_USER_WARNING);
}
}
/**
* Initialize v2 loop variable.
*
* @param string $stack_id
* @param array|Traversable &$array
* @return object
*/
protected function _v2_initLoopVar(string $stack_id, &$array): object
{
// Create the data structure.
$loop = new \stdClass;
$loop->index = 0;
$loop->iteration = 1;
$loop->count = is_countable($array) ? count($array) : countobj($array);
$loop->remaining = $loop->count - 1;
$loop->first = true;
$loop->last = ($loop->count === 1);
$loop->even = false;
$loop->odd = true;
$loop->depth = count(self::$_loopvars) + 1;
$loop->parent = count(self::$_loopvars) ? end(self::$_loopvars) : null;
// Append to stack and return.
return self::$_loopvars[$stack_id] = $loop;
}
/**
* Increment v2 loop variable.
*
* @param object $loopvar
* @return void
*/
protected function _v2_incrLoopVar(object $loop): void
{
// Update properties.
$loop->index++;
$loop->iteration++;
$loop->remaining--;
$loop->first = ($loop->count === 1);
$loop->last = ($loop->iteration === $loop->count);
$loop->even = ($loop->iteration % 2 === 0);
$loop->odd = !$loop->even;
}
/**
* Remove v2 loop variable.
*
* @param object $loopvar
* @return void
*/
protected function _v2_removeLoopVar(object $loop): void
{
// Remove from stack.
if ($loop === end(self::$_loopvars))
{
array_pop(self::$_loopvars);
}
}
/**
* Attribute builder for v2.
*
* @param string $attribute
* @param array $definition
* @return string
*/
protected function _v2_buildAttribute(string $attribute, array $definition = []): string
{
$delimiters = [
'class' => ' ',
'style' => '; ',
];
$values = [];
foreach ($definition as $key => $val)
{
if (is_int($key) && !empty($val))
{
$values[] = $val;
}
elseif ($val)
{
$values[] = $key;
}
}
return sprintf(' %s="%s"', $attribute, escape(implode($delimiters[$attribute], $values), false));
}
/**
* Auth checker for v2.
*
* @param string $type
* @return bool
*/
protected function _v2_checkAuth(string $type = 'member'): bool
{
$grant = \Context::get('grant');
switch ($type)
{
case 'admin': return $this->user->isAdmin();
case 'manager': return $grant->manager ?? false;
case 'member': return $this->user->isMember();
default: false;
}
}
/**
* Capability checker for v2.
*
* @param int $check_type
* @param string|array $capability
* @return bool
*/
protected function _v2_checkCapability(int $check_type, $capability): bool
{
$grant = \Context::get('grant');
if ($check_type === 1)
{
return isset($grant->$capability) ? boolval($grant->$capability) : false;
}
elseif ($check_type === 2)
{
return isset($grant->$capability) ? !boolval($grant->$capability) : true;
}
elseif (is_array($capability))
{
foreach ($capability as $cap)
{
if (isset($grant->$cap) && $grant->$cap)
{
return true;
}
}
return false;
}
else
{
return false;
}
}
/**
* Check if a validation error exists for v2.
*
* @param ...$args
* @return bool
*/
protected function _v2_errorExists(...$args): bool
{
$validator_id = \Context::get('XE_VALIDATOR_ID');
$validator_message = \Context::get('XE_VALIDATOR_MESSAGE');
if (empty($validator_id) || empty($validator_message))
{
return false;
}
return count($args) ? in_array((string)$validator_id, $args, true) : true;
}
}

View file

@ -0,0 +1,911 @@
<?php
namespace Rhymix\Framework\Parsers\Template;
use HTMLDisplayHandler;
use Rhymix\Framework\Template;
/**
* Template parser v1 for XE compatibility.
*
* Originally part of TemplateHandler, this parser is preserved here
* for bug-for-bug compatibility with XE and older versions of Rhymix.
* A significant part of this code dates back to the early days of XE,
* though Rhymix has managed to squeeze in a few new features.
*
* Except in the case of a serious security issue, there will be no change
* to the parsing and conversion logic, and no new features.
* It is strongly recommended that new templates be written in v2.
*/
class TemplateParser_v1
{
/**
* Instance properties.
*/
public $autoescape_config_exists;
public $source_type;
public $template;
/**
* Convert template code into PHP.
*
* @param string $content
* @param Template $template
* @return string
*/
public function convert(string $content, Template $template): string
{
// Prepare default settings.
ini_set('pcre.jit', false);
$this->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)\)--|\/)>|<!--(@[a-z@]*)([\s\S]*?)-->(\s*)/', array($this, '_parseResource'), $content);
// remove block which is a virtual tag
$content = preg_replace('@</?block\s*>@is', '', $content);
// form auto generation
$temp = preg_replace_callback('/(<form(?:<\?php.+?\?>|[^<>]+)*?>)(.*?)(<\/form>)/is', array($this, '_compileFormAuthGeneration'), $content);
if($temp)
{
$content = $temp;
}
// prevent from calling directly before writing into file
$content = '<?php if (!defined("RX_VERSION")) exit();?>' . $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<!--)/',
'/\n[\t\x20]*?(?=\n<!--)/',
'/\n[\t\x20]+?\<\?php/',
], [
"><?php } ?>\n<?php echo \"\\n\"; ?>",
"\n<?php ?>",
"\n\t<?php",
], $content);
return $content;
}
/**
* preg_replace_callback handler
* 1. remove ruleset from form tag
* 2. add hidden tag with ruleset value
* 3. if empty default hidden tag, generate hidden tag (ex:mid, act...)
* 4. generate return url, return url use in server side validator
* @param array $matches
* @return string
*/
private function _compileFormAuthGeneration($matches)
{
// check rx-autoform attribute
if (preg_match('/\srx-autoform="([^">]*?)"/', $matches[1], $m1))
{
$autoform = toBool($m1[1]);
$matches[1] = preg_replace('/\srx-autoform="([^">]*?)"/', '', $matches[1]);
}
else
{
$autoform = true;
}
// form ruleset attribute move to hidden tag
if ($autoform && $matches[1])
{
preg_match('/ruleset="([^"]*?)"/is', $matches[1], $m);
if(isset($m[0]) && $m[0])
{
$matches[1] = preg_replace('/' . addcslashes($m[0], '?$') . '/i', '', $matches[1]);
if(strpos($m[1], '@') !== FALSE)
{
$path = str_replace('@', '', $m[1]);
$path = './files/ruleset/' . $path . '.xml';
$autoPath = '';
}
else if(strpos($m[1], '#') !== FALSE)
{
$fileName = str_replace('#', '', $m[1]);
$fileName = str_replace('<?php echo ', '', $fileName);
$fileName = str_replace(' ?>', '', $fileName);
$path = '#./files/ruleset/' . $fileName . '.xml';
preg_match('@(?:^|\.?/)(modules/[\w-]+)@', $this->template->relative_path, $mm);
$module_path = $mm[1];
list($rulsetFile) = explode('.', $fileName);
$autoPath = $module_path . '/ruleset/' . $rulsetFile . '.xml';
$m[1] = $rulsetFile;
}
else if(preg_match('@(?:^|\.?/)(modules/[\w-]+)@', $this->template->relative_path, $mm))
{
$module_path = $mm[1];
$path = $module_path . '/ruleset/' . $m[1] . '.xml';
$autoPath = '';
}
$matches[2] = '<input type="hidden" name="ruleset" value="' . $m[1] . '" />' . $matches[2];
//assign to addJsFile method for js dynamic recache
$matches[1] = '<?php Context::addJsFile("' . $path . '", FALSE, "", 0, "body", TRUE, "' . $autoPath . '") ?' . '>' . $matches[1];
}
}
// if not exists default hidden tag, generate hidden tag
if ($autoform)
{
preg_match_all('/<input[^>]* name="(act|mid)"/is', $matches[2], $m2);
$missing_inputs = array_diff(['act', 'mid'], $m2[1]);
if(is_array($missing_inputs))
{
$generatedHidden = '';
foreach($missing_inputs as $key)
{
$generatedHidden .= '<input type="hidden" name="' . $key . '" value="<?php echo $__Context->' . $key . ' ?? \'\'; ?>" />';
}
$matches[2] = $generatedHidden . $matches[2];
}
}
// return url generate
if ($autoform)
{
if (!preg_match('/no-(?:error-)?return-url="true"/i', $matches[1]))
{
preg_match('/<input[^>]*name="error_return_url"[^>]*>/is', $matches[2], $m3);
if(!isset($m3[0]) || !$m3[0])
{
$matches[2] = '<input type="hidden" name="error_return_url" value="<?php echo escape(getRequestUriByServerEnviroment(), false); ?>" />' . $matches[2];
}
}
else
{
$matches[1] = preg_replace('/no-(?:error-)?return-url="true"/i', '', $matches[1]);
}
}
array_shift($matches);
return implode('', $matches);
}
/**
* preg_replace_callback handler
*
* replace image path
* @param array $match
*
* @return string changed result
*/
private function _replacePath($match)
{
$src = $this->_replaceRelativePath($match);
return substr($match[0], 0, -strlen($match[1]) - 6) . "src=\"{$src}\"";
}
/**
* replace relative path
* @param array $match
*
* @return string changed result
*/
private function _replaceRelativePath($match)
{
//return origin code when src value started '${'.
if(preg_match('@^\${@', $match[1]))
{
return $match[1];
}
//return origin code when src value include variable.
if(preg_match('@^[\'|"]\s*\.\s*\$@', $match[1]))
{
return $match[0];
}
$src = preg_replace('@^(\./)+@', '', trim($match[1]));
$src = \RX_BASEURL . $this->template->relative_dirname . $src;
$src = str_replace('/./', '/', $src);
// for backward compatibility
$src = preg_replace('@/((?:[\w-]+/)+)\1@', '/\1', $src);
while(($tmp = preg_replace('@[^/]+/\.\./@', '', $src, 1)) !== $src)
{
$src = $tmp;
}
return $src;
}
/**
* preg_replace_callback handler
*
* replace srcset string with multiple paths
* @param array $match
*
* @return string changed result
*/
private function _replaceSrcsetPath($match)
{
// explode urls by comma
$url_list = explode(",", $match[1]);
foreach ($url_list as &$url) {
// replace if url is not starting with the pattern
$url = preg_replace_callback(
'/^(?!(?:https?|file|data):|[\/\{])(\S+)/i',
array($this, '_replaceRelativePath'),
trim($url)
);
}
$srcset = implode(", ", $url_list);
return substr($match[0], 0, -strlen($match[1]) - 9) . "srcset=\"{$srcset}\"";
}
/**
* replace loop and cond template syntax
* @param string $content
* @return string changed result
*/
private function _parseInline($content)
{
// list of self closing tags
$self_closing = array('area' => 1, 'base' => 1, 'basefont' => 1, 'br' => 1, 'hr' => 1, 'input' => 1, 'img' => 1, 'link' => 1, 'meta' => 1, 'param' => 1, 'frame' => 1, 'col' => 1);
$skip = sprintf('(?!%s)', implode('|', ['marquee']));
$split_regex = "@(</?{$skip}[a-zA-Z](?>[^<>{}\"]+|<!--.*?-->.*?<!--.*?end-->|{[^}]*}|\"(?>'.*?'|.)*?\"|.)*?>)@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] .= "<?php if({$expr}){ ?>";
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('<?php $__loop_tmp=%1$s;if($__loop_tmp)foreach($__loop_tmp as %2$s){ ?>', $expr_m[1], $expr_m[2]);
}
elseif(isset($expr_m[4]) && $expr_m[4])
{
$nodes[$idx - 1] .= "<?php for({$expr_m[4]}){ ?>";
}
elseif(isset($expr_m[5]) && $expr_m[5])
{
$nodes[$idx - 1] .= "<?php while({$expr_m[5]}={$expr_m[6]}){ ?>";
}
break;
}
}
$node = preg_replace('@\s(loop|cond)="([^"]+)"@', '', $node);
// find closing tag
$close_php = '<?php ' . str_repeat('}', $closing) . ' ?>';
// 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', '<?php if($2){ ?>$1<?php } ?>', $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 "<?php {$m[1]} ?>";
}
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('@^(.+?)(?<![|\s])((?:\|[a-z]{2}[a-z0-9_]+(?::.+)?)+)$@', $m[1], $mm))
{
$m[1] = $mm[1];
$filters = array_map('trim', explode_with_escape('|', substr($mm[2], 1)));
}
else
{
$filters = array();
}
// Process the variable.
$var = self::_replaceVar($m[1]);
// Apply filters.
foreach ($filters as $filter)
{
// Separate filter option from the filter name.
if (preg_match('/^([a-z0-9_-]+):(.+)$/', $filter, $matches))
{
$filter = $matches[1];
$filter_option = $matches[2];
if (!self::_isVar($filter_option) && !preg_match("/^'.*'$/", $filter_option) && !preg_match('/^".*"$/', $filter_option))
{
$filter_option = "'" . escape_sqstr($filter_option) . "'";
}
else
{
$filter_option = self::_replaceVar($filter_option);
}
}
else
{
$filter_option = null;
}
// Apply each filter.
switch ($filter)
{
case 'auto':
case 'autoescape':
case 'autolang':
case 'escape':
case 'noescape':
$escape_option = $filter;
break;
case 'escapejs':
$var = "escape_js({$var})";
break;
case 'json':
$var = "json_encode({$var})";
break;
case 'strip':
case 'strip_tags':
$var = $filter_option ? "strip_tags({$var}, {$filter_option})" : "strip_tags({$var})";
break;
case 'trim':
$var = "trim({$var})";
break;
case 'urlencode':
$var = "rawurlencode({$var})";
break;
case 'lower':
$var = "strtolower({$var})";
break;
case 'upper':
$var = "strtoupper({$var})";
break;
case 'nl2br':
$var = $this->_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 = "'<a href=\"' . ($filter_option) . '\">' . ($var) . '</a>'";
}
else
{
$var = "'<a href=\"' . ($var) . '\">' . ($var) . '</a>'";
}
$escape_option = 'noescape';
break;
default:
$filter = escape_sqstr($filter);
$var = "'INVALID FILTER ({$filter})'";
}
}
// Apply the escape option and return.
return '<?php echo ' . $this->_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])
{
// <!--#include--> or <include ..>
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 "<?php \$__tpl=TemplateHandler::getInstance();echo \$__tpl->compile('{$fileDir}','{$pathinfo['basename']}') ?>";
// <!--%load_js_plugin-->
case 'load_js_plugin':
$plugin = self::_replaceVar($m[5]);
$s = "<!--#JSPLUGIN:{$plugin}-->";
if(strpos($plugin, '$__Context') === false)
{
$plugin = "'{$plugin}'";
}
$s .= "<?php Context::loadJavascriptPlugin({$plugin}); ?>";
return $s;
// <load ...> or <unload ...> or <!--%import ...--> or <!--%unload ...-->
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 = "<?php {$result} ?>";
if($metafile)
{
if(!$metavars)
{
$result = "<!--#Meta:{$metafile}-->" . $result;
}
else
{
// LESS or SCSS needs the variables to be substituted.
$result = "<!--#Meta:{$metafile}?{$metavars}-->" . $result;
}
}
return $result;
// <config ...>
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 "<?php {$result} ?>";
}
}
// <!--@..--> such as <!--@if($cond)-->, <!--@else-->, <!--@end-->
if($m[7])
{
$m[7] = substr($m[7], 1);
if(!$m[7])
{
return '<?php ' . self::_replaceVar($m[8]) . '{ ?>' . $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 '<?php } ?>' . $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 '<?php ' . self::_replaceVar($precheck . $m[7] . $m[8]) . '{ ?>' . $m[9];
}
if($mm[2])
{
return "<?php }{$m[7]}" . self::_replaceVar($m[8]) . "{ ?>" . $m[9];
}
if($mm[4])
{
return "<?php " . ($mm[3] ? 'break;' : '') . "{$m[7]} " . trim($m[8], '()') . ": ?>" . $m[9];
}
if($mm[5])
{
return "<?php break; ?>";
}
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('@(?<!::|\\\\|(?<!eval\()\')\$([a-z_][a-z0-9_]*)@i', $str) ? true : false;
}
/**
* Replace PHP variables of $ character
*
* @param string $php
* @return string
*/
private static function _replaceVar($php)
{
if(!strlen($php))
{
return '';
}
// Replace variables that need to be enclosed in curly braces, using temporary entities to prevent double-replacement.
$php = preg_replace_callback('@(?<!\$__Context)->\$([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('@(?<!::|\\\\|\$__Context->|(?<!eval\()\')\$([a-z_][a-z0-9_]*)@i', function($matches) {
if (preg_match('/^(?:GLOBALS|_SERVER|_COOKIE|_ENV|_GET|_POST|_REQUEST|_SESSION|__Context|this)$/', $matches[1]))
{
return '$' . $matches[1];
}
else
{
return '$__Context->' . $matches[1];
}
}, $php);
return $php;
}
/**
* Replace temporary entities to curly braces.
*
* @param string $str
* @return string
*/
private static function _replaceTempEntities($str)
{
return strtr($str, [
'&#x1B;&#x7B;' => '{',
'&#x1B;&#x7D;' => '}',
]);
}
/**
* Get the temporary entity for a character.
*
* @param string $char
* @return string
*/
private static function _getTempEntityForChar($char)
{
return '&#x1B;&#x' . strtoupper(bin2hex($char)) . ';';
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
location ~ ^/rhymix/modules/editor/(skins|styles)/.+\.html$ {
# pass
}
location ~ ^/rhymix/(addons|common/tpl|files/ruleset|(m\.)?layouts|modules|plugins|themes|widgets|widgetstyles)/.+\.(html|xml)$ {
location ~ ^/rhymix/(addons|common/tpl|files/ruleset|(m\.)?layouts|modules|plugins|themes|widgets|widgetstyles)/.+\.(html|xml|blade\.php)$ {
return 403;
}
location ~ ^/rhymix/files/(attach|config|cache)/.+\.(ph(p|t|ar)?[0-9]?|p?html?|cgi|pl|exe|[aj]spx?|inc|bak)$ {

View file

@ -2,7 +2,7 @@
location ~ ^/modules/editor/(skins|styles)/.+\.html$ {
# pass
}
location ~ ^/(addons|common/tpl|files/ruleset|(m\.)?layouts|modules|plugins|themes|widgets|widgetstyles)/.+\.(html|xml)$ {
location ~ ^/(addons|common/tpl|files/ruleset|(m\.)?layouts|modules|plugins|themes|widgets|widgetstyles)/.+\.(html|xml|blade\.php)$ {
return 403;
}
location ~ ^/files/(attach|config|cache)/.+\.(ph(p|t|ar)?[0-9]?|p?html?|cgi|pl|exe|[aj]spx?|inc|bak)$ {

View file

@ -1,3 +1,3 @@
<script>
top.location.reload();
</script>
top.location.reload();
</script>

View file

@ -361,6 +361,8 @@ class Cleanup extends Base
'modules/spamfilter/spamfilter.lib.php' => 'deleted',
'modules/spamfilter/ruleset/' => 'deleted',
'phpDoc/' => 'deleted:xe',
'tests/unit/classes/template/' => 'deleted',
'tests/unit/classes/TemplateHandlerTest.php' => 'deleted',
'tools/dbxml_validator/' => 'deleted:xe',
'tools/korea_ip_ranges/' => 'deleted',
'tools/phpDoc/' => 'deleted',

View file

@ -260,7 +260,7 @@ class PageAdminView extends Page
$security->encodeHTML('widget_list..title','module_info.mid');
// Load admin resources
$oTemplate = TemplateHandler::getInstance();
$oTemplate = Rhymix\Framework\Template::getInstance();
$oTemplate->compile('modules/admin/tpl', '_admin_common.html');
// Set a template file

View file

@ -5,8 +5,6 @@ class PageMobile extends PageView
{
function _getArticleContent()
{
$oTemplate = &TemplateHandler::getInstance();
$oDocument = DocumentModel::getDocument(0);
if($this->module_info->mdocument_srl ?? 0)
{
@ -22,6 +20,7 @@ class PageMobile extends PageView
}
Context::set('oDocument', $oDocument);
$oTemplate = Rhymix\Framework\Template::getInstance();
$template_path = $this->getTemplatePath();
if (preg_match('!/skins/!', $template_path))
{

View file

@ -141,8 +141,6 @@ class PageView extends Page
function _getArticleContent()
{
$oTemplate = &TemplateHandler::getInstance();
$oDocument = DocumentModel::getDocument(0);
if($this->module_info->document_srl ?? 0)
{
@ -152,6 +150,7 @@ class PageView extends Page
}
Context::set('oDocument', $oDocument);
$oTemplate = Rhymix\Framework\Template::getInstance();
$page_content = $oTemplate->compile($this->getTemplatePath(), 'content');
return $page_content;
@ -252,7 +251,7 @@ class PageView extends Page
if ($this->proc_tpl)
{
// Store compiled template in a temporary file.
$oTemplate = TemplateHandler::getInstance();
$oTemplate = Rhymix\Framework\Template::getInstance();
$real_target_dir = dirname($real_target_file);
$tmp_cache_file = preg_replace('/\.cache\.php$/', '.compiled.php', $cache_file);
$content = $oTemplate->compileDirect($real_target_dir . '/', basename($real_target_file));

View file

@ -747,7 +747,7 @@ class WidgetController extends Widget
}
// Compilation
$widgetstyle_path = $oWidgetModel->getWidgetStylePath($widgetStyle);
$oTemplate = &TemplateHandler::getInstance();
$oTemplate = Rhymix\Framework\Template::getInstance();
$tpl = $oTemplate->compile($widgetstyle_path, 'widgetstyle');
return $tpl;

View file

@ -0,0 +1,5 @@
.foo {
.bar {
width: 100%;
}
}

View file

View file

@ -0,0 +1 @@
<div>{$var}</div>

View file

@ -0,0 +1 @@
<div>Empty</div>

View file

@ -0,0 +1,6 @@
<config version="2" />
<div>{{ $foobar }}</div>
<div>{{ $globalonly }}</div>
@once
@load ('../js/test.js')
@endonce

View file

@ -0,0 +1,5 @@
<config version="2" />
<div class="self">{{ $foobar }}</div>
<div class="incl">
<include src="scopetest1.html" vars="['foobar' => 'Included #3']" />
</div>

View file

@ -0,0 +1,3 @@
(function($) {
// TEST
})(jQuery);

View file

@ -0,0 +1,66 @@
<?php if (!defined("RX_VERSION")) exit(); ?><?php $this->config->version = 2; ?>
<div><?php echo $this->_v2_include('include', '^/common/tpl/refresh.html'); ?></div>
<div><?php $this->_v2_loadResource('^/common/js/plugins/ckeditor/'); ?></div>
<?php $this->_v2_loadResource('css/style.scss', 'print', '', []); ?>
<?php
$__Context->foo = 'FOOFOO<"FOO">BAR';
?>
<?php
$__Context->bar = ['Rhy', 'miX', 'is', 'da', 'BEST!'];
?>
{{ $foo }}
<form action="<?php echo $this->config->context === 'JS' ? escape_js(\RX_BASEURL) : htmlspecialchars(\RX_BASEURL, \ENT_QUOTES, 'UTF-8', false); ?>" method="post">
<input type="hidden" name="_rx_csrf_token" value="<?php echo \Rhymix\Framework\Session::getGenericToken(); ?>" />
<input type="text"<?php if (Context::getInstance()->get('foo')): ?> required="required"<?php endif; ?>>
<input type="text" value="<?php echo $this->config->context === 'JS' ? escape_js($__Context->bar[0] ?? '') : htmlspecialchars($__Context->bar[0] ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"<?php if ($__Context->bar[3] === 'da'): ?> required="required"<?php endif; ?> />
</form>
<div<?php if (!(isset($__Context->baz))): ?> class="foobar"<?php endif; ?>>
<?php if ($__Context->foo || $__Context->bar): ?>
<p>Hello <?php if ($__Context->bar): ?><?php echo $__Context->foo ?? ''; ?><?php endif; ?></p>
<p><?php echo $this->config->context === 'JS' ? escape_js(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar))) : htmlspecialchars(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar)), \ENT_QUOTES, 'UTF-8', false); ?></p>
<?php endif; ?>
</div>
<?php ob_start(); $__last_fragment_name = 'rhymix'; ?>
<?php $__tmp_RANDOM_LOOP_ID = Context::get('bar') ?? []; if($__tmp_RANDOM_LOOP_ID): $__loop_RANDOM_LOOP_ID = $this->_v2_initLoopVar("RANDOM_LOOP_ID", $__tmp_RANDOM_LOOP_ID); foreach ($__tmp_RANDOM_LOOP_ID as $__Context->k => $__Context->val): ?>
<div>
<?php if (empty($__Context->nosuchvar)): ?>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span <?php if ($__Context->k >= 2): ?>class="<?php echo $this->config->context === 'JS' ? escape_js($__Context->val ?? '') : htmlspecialchars($__Context->val ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"<?php endif; ?>></span>
<?php endif; ?>
</div>
<?php $this->_v2_incrLoopVar($__loop_RANDOM_LOOP_ID); endforeach; $this->_v2_removeLoopVar($__loop_RANDOM_LOOP_ID); unset($__loop_RANDOM_LOOP_ID); else: ?><div>Nothing here...</div><?php endif; ?>
<?php $this->_fragments[$__last_fragment_name] = ob_get_flush(); ?>
<?php (function($__filename, $__vars, $__varname, $__empty = null) { if (!$__vars): $__vars = []; if ($__empty): $__filename = $__empty; $__vars[] = ''; endif; endif; foreach ($__vars as $__var): echo $this->_v2_include("include", $__filename, [(string)$__varname => $__var]); endforeach; })('incl/eachtest', $__Context->bar, 'var'); ?>
<?php (function($__filename, $__vars, $__varname, $__empty = null) { if (!$__vars): $__vars = []; if ($__empty): $__filename = $__empty; $__vars[] = ''; endif; endif; foreach ($__vars as $__var): echo $this->_v2_include("include", $__filename, [(string)$__varname => $__var]); endforeach; })('incl/eachtest', [], 'anything', 'incl/empty'); ?>
<?php if (!\Context::get('m')): ?>
<p>The full class name is <?php echo htmlspecialchars(get_class(new Rhymix\Framework\Push), \ENT_QUOTES, 'UTF-8', true); ?>, <?php echo $this->config->context === 'JS' ? escape_js(Rhymix\Framework\Push::class) : htmlspecialchars(Rhymix\Framework\Push::class, \ENT_QUOTES, 'UTF-8', false); ?> really.</p>
<?php endif; ?>
<div class="barContainer" data-bar="<?php echo $this->config->context === 'JS' ? json_encode($__Context->bar ?? '', self::$_json_options) : htmlspecialchars(json_encode($__Context->bar ?? '', self::$_json_options), \ENT_QUOTES, 'UTF-8', false); ?>">
<span<?php echo $this->_v2_buildAttribute('class', [
'a-1',
'font-normal' => $__Context->foo,
'text-blue' => false,
'bg-gray-200' => true
]); ?>></span>
<span<?php echo $this->_v2_buildAttribute('style', [
'border-radius: 0.25rem',
'margin: 1rem' => Context::get('bar'),
'padding: 2rem' => false,
]); ?>></span>
</div>
<script type="text/javascript"<?php $this->config->context = "JS"; ?>>
const foo = '<?php echo $this->config->context === 'JS' ? escape_js($__Context->foo ?? '') : htmlspecialchars($__Context->foo ?? '', \ENT_QUOTES, 'UTF-8', false); ?>';
const bar = <?php echo $this->config->context === 'JS' ? json_encode($__Context->bar, self::$_json_options) : htmlspecialchars(json_encode($__Context->bar, self::$_json_options), \ENT_QUOTES, 'UTF-8', false); ?>;
<?php $this->config->context = "HTML"; ?></script>

View file

@ -0,0 +1,62 @@
<div><script>
top.location.reload();
</script>
</div>
<div></div>
{{ $foo }}
<form action="/rhymix/" method="post">
<input type="hidden" name="_rx_csrf_token" value="" />
<input type="text" required="required">
<input type="text" value="Rhy" required="required" />
</form>
<div class="foobar">
<p>Hello FOOFOO<"FOO">BAR</p>
<p>RHY|MIX|IS|DA|BEST!</p>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span ></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span ></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span class="is"></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span class="da"></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span class="BEST!"></span>
</div>
<div>Rhy</div>
<div>miX</div>
<div>is</div>
<div>da</div>
<div>BEST!</div>
<div>Empty</div>
<p>The full class name is Rhymix\Framework\Push, Rhymix\Framework\Push really.</p>
<div class="barContainer" data-bar="[&quot;Rhy&quot;,&quot;miX&quot;,&quot;is&quot;,&quot;da&quot;,&quot;BEST!&quot;]">
<span class="a-1 font-normal bg-gray-200"></span>
<span style="border-radius: 0.25rem; margin: 1rem"></span>
</div>
<script type="text/javascript">
const foo = 'FOOFOO\u003C\u0022FOO\u0022\u003EBAR';
const bar = ["Rhy","miX","is","da","BEST!"];
</script>

View file

@ -0,0 +1,21 @@
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span ></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span ></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span class="is"></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span class="da"></span>
</div>
<div>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" />
<span class="BEST!"></span>
</div>

View file

@ -0,0 +1,66 @@
<config version="2" />
@use('Rhymix\Framework\Push', 'Push')
<div>@include('^/common/tpl/refresh.html')</div>
<div>@load('^/common/js/plugins/ckeditor/')</div>
<load src="css/style.scss" media="print" />
<?php
$foo = 'FOOFOO<"FOO">BAR';
?>
@php
$bar = ['Rhy', 'miX', 'is', 'da', 'BEST!'];
@endphp
@verbatim
{{ $foo }}
@endverbatim
<form action="{\RX_BASEURL}" method="post">
@csrf
<input type="text" @required(Context::getInstance()->get('foo'))>
<input type="text" value="{$bar[0]}" required|if="$bar[3] === 'da'" />
</form>
<div class="foobar"|unless="isset($baz)">
@if ($foo || $bar)
<p>Hello @if ($bar){$foo|noescape}@endif</p>
<p>{{ implode('|', array_map(function(\$i) { return strtoupper(\$i); }, $bar)) }}</p>
@end
</div>
@fragment('rhymix')
@forelse (Context::get('bar') as $k => $val)
<div>
@empty ($nosuchvar)
<img src="foo/../bar/rhymix.svg" alt="unit tests are cool" />
<span <!--@if($k >= 2)-->class="{$val}"<!--@end-->></span>
@endempty
</div>
@empty<div>Nothing here...</div>@end
@endfragment
@each('incl/eachtest', $bar, 'var')
@each('incl/eachtest', [], 'anything', 'incl/empty')
@desktop
<p>The full class name is {get_class(new Push)|escape}, {Push::class} really.</p>
@enddesktop
<div class="barContainer" data-bar="{$bar|json}">
<span @class([
'a-1',
'font-normal' => $foo,
'text-blue' => false,
'bg-gray-200' => true
])></span>
<span @style([
'border-radius: 0.25rem',
'margin: 1rem' => Context::get('bar'),
'padding: 2rem' => false,
])></span>
</div>
<script type="text/javascript">
const foo = '{{ $foo }}';
const bar = @json($bar);
</script>

View file

@ -0,0 +1,179 @@
<h1>Pets</h1>
<ul>
<li>
<span>A red dog</span>
<span>current index 0 of 5</span>
<span>parent index 0 of 5</span>
<span>first: bool(true)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A red cat</span>
<span>current index 1 of 5</span>
<span>parent index 0 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A red rabbit</span>
<span>current index 2 of 5</span>
<span>parent index 0 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A red panda</span>
<span>current index 3 of 5</span>
<span>parent index 0 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A red otter</span>
<span>current index 4 of 5</span>
<span>parent index 0 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(true)</span>
</li>
<li>
<span>A blue dog</span>
<span>current index 0 of 5</span>
<span>parent index 1 of 5</span>
<span>first: bool(true)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A blue cat</span>
<span>current index 1 of 5</span>
<span>parent index 1 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A blue rabbit</span>
<span>current index 2 of 5</span>
<span>parent index 1 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A blue panda</span>
<span>current index 3 of 5</span>
<span>parent index 1 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A blue otter</span>
<span>current index 4 of 5</span>
<span>parent index 1 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(true)</span>
</li>
<li>
<span>A yellow dog</span>
<span>current index 0 of 5</span>
<span>parent index 2 of 5</span>
<span>first: bool(true)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A yellow cat</span>
<span>current index 1 of 5</span>
<span>parent index 2 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A yellow rabbit</span>
<span>current index 2 of 5</span>
<span>parent index 2 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A yellow panda</span>
<span>current index 3 of 5</span>
<span>parent index 2 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A yellow otter</span>
<span>current index 4 of 5</span>
<span>parent index 2 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(true)</span>
</li>
<li>
<span>A black dog</span>
<span>current index 0 of 5</span>
<span>parent index 3 of 5</span>
<span>first: bool(true)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A black cat</span>
<span>current index 1 of 5</span>
<span>parent index 3 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A black rabbit</span>
<span>current index 2 of 5</span>
<span>parent index 3 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A black panda</span>
<span>current index 3 of 5</span>
<span>parent index 3 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A black otter</span>
<span>current index 4 of 5</span>
<span>parent index 3 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(true)</span>
</li>
<li>
<span>A white dog</span>
<span>current index 0 of 5</span>
<span>parent index 4 of 5</span>
<span>first: bool(true)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A white cat</span>
<span>current index 1 of 5</span>
<span>parent index 4 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A white rabbit</span>
<span>current index 2 of 5</span>
<span>parent index 4 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A white panda</span>
<span>current index 3 of 5</span>
<span>parent index 4 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(false)</span>
</li>
<li>
<span>A white otter</span>
<span>current index 4 of 5</span>
<span>parent index 4 of 5</span>
<span>first: bool(false)</span>
<span>last: bool(true)</span>
</li>
</ul>

View file

@ -0,0 +1,22 @@
@version(2)
@php
$foo = ['red', 'blue', 'yellow', 'black', 'white'];
$bar = ['dog', 'cat', 'rabbit', 'panda', 'otter'];
@endphp
<h1>Pets</h1>
<ul>
@foreach ($foo as $color)
@foreach ($bar as $animal)
<li>
<span>A {{ $color }} {{ $animal }}</span>
<span>current index {{ $loop->index }} of {{ $loop->count }}</span>
<span>parent index {{ $loop->parent->index }} of {{ $loop->parent->count }}</span>
<span>first: @dump($loop->first)</span>
<span>last: @dump($loop->last)</span>
</li>
@endforeach
@endforeach
</ul>

View file

@ -0,0 +1,7 @@
<ul>
<li>Rhymix</li>
<li>XE</li>
<li>WordPress</li>
<li>Drupal</li>
</ul>

View file

@ -0,0 +1,35 @@
@version(2)
<?php $foo = 42; ?>
@pushOnce('cms')
<li>XE</li>
@endpushOnce
@pushonce('cms')
<li>XE</li>
@endPushOnce
@push('cms')
<li>WordPress</li>
@end
@pushIf($foo, 'cms')
<li>Drupal</li>
@endPushIf
@prependOnce('cms')
<li>Rhymix</li>
@endPrependOnce
@prependonce('cms')
<li>Rhymix</li>
@endprependonce
@prependIf(!$foo, 'cms')
<li>Joomla</li>
@end
<ul>
@stack('cms')
</ul>

View file

@ -0,0 +1,11 @@
<div class="alert">
<p>You have an error!</p>
<p><strong>[Ref. #2]</strong></p>
</div>
<div class="alert">
<p>You have an error!</p>
<p><strong>[Ref. #4]</strong></p>
</div>

View file

@ -0,0 +1,41 @@
@version(2)
<!--// No error yet -->
@error('foo/bar/baz/1')
<div class="alert">
<p>{$XE_VALIDATOR_MESSAGE}</p>
<p><strong>[Ref. #1]</strong></p>
</div>
@enderror
<!--// We set error ID and message now -->
{@ $XE_VALIDATOR_ID = 'foo/bar/baz/1'}
{@ $XE_VALIDATOR_MESSAGE = 'You have an error!'}
<!--// This should work -->
@error('foo/bar/baz/3', 'foo/bar/baz/2', 'foo/bar/baz/1')
<div class="alert">
<p>{$XE_VALIDATOR_MESSAGE}</p>
<p><strong>[Ref. #2]</strong></p>
</div>
@enderror
<!--// Wrong error ID -->
@error('foo/bar/baz/6', 'foo/bar/baz/5', 'foo/bar/baz/4')
<div class="alert">
<p>{$XE_VALIDATOR_MESSAGE}</p>
<p><strong>[Ref. #3]</strong></p>
</div>
@enderror
<!--// Check for any error, this should also work -->
@error
<div class="alert">
<p>{$XE_VALIDATOR_MESSAGE}</p>
<p><strong>[Ref. #4]</strong></p>
</div>
@enderror
<!--// Cleanup -->
{@ $XE_VALIDATOR_ID = null}
{@ $XE_VALIDATOR_MESSAGE = null}

View file

@ -0,0 +1,18 @@
<div class="global">
<div>Rhymix Template</div>
<div>Context Variable</div>
</div>
<div class="test1">
<div>Included #1</div>
<div></div>
</div>
<div class="test2">
<div class="self">Included #2</div>
<div class="incl">
<div>Included #3</div>
<div></div>
</div>
</div>

View file

@ -0,0 +1,18 @@
@version(2)
<?php
$foobar = 'Rhymix Template';
$globalonly = 'Context Variable';
?>
<div class="global">
@include ('incl/scopetest1.html')
</div>
<div class="test1">
@include ('incl/scopetest1.html', ['foobar' => 'Included #1'])
</div>
<div class="test2">
<include src="incl/scopetest2.html" vars="['foobar' => 'Included #2']" />
</div>

View file

@ -0,0 +1,67 @@
<?php
class TemplateTest extends \Codeception\Test\Unit
{
public function _before()
{
Context::init();
}
public function testIsRelativePath()
{
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html');
$this->assertTrue($tmpl->isRelativePath('foo.html'));
$this->assertTrue($tmpl->isRelativePath('foo/bar.html'));
$this->assertTrue($tmpl->isRelativePath('../foo/bar.html'));
$this->assertTrue($tmpl->isRelativePath('foo/../bar.html'));
$this->assertTrue($tmpl->isRelativePath('^/foo/../bar.html'));
$this->assertFalse($tmpl->isRelativePath('/foo/bar.html'));
$this->assertFalse($tmpl->isRelativePath('https://foo.com/bar.html'));
$this->assertFalse($tmpl->isRelativePath('file:///C:/foo/bar.html'));
$this->assertFalse($tmpl->isRelativePath('data:image/png;base64,AAAAAAAAAAA="'));
$this->assertFalse($tmpl->isRelativePath('{$foo}'));
}
public function testConvertPath()
{
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html');
$source = 'foo.html';
$target = 'tests/_data/template/foo.html';
$this->assertEquals($target, $tmpl->convertPath($source));
$source = 'foo/bar.js';
$target = 'tests/_data/template/foo/bar.js';
$this->assertEquals($target, $tmpl->convertPath($source));
$source = '../foo.scss';
$target = 'tests/_data/foo.scss';
$this->assertEquals($target, $tmpl->convertPath($source));
$source = '../../_output/foo/../bar.jpg';
$target = 'tests/_output/bar.jpg';
$this->assertEquals($target, $tmpl->convertPath($source));
$source = '/foo/bar.blade.php';
$target = 'tests/_data/template/foo/bar.blade.php';
$this->assertEquals($target, $tmpl->convertPath($source));
$source = '^/foo/bar.gif';
$target = '/rhymix/foo/bar.gif';
$this->assertEquals($target, $tmpl->convertPath($source));
}
public function testNormalizePath()
{
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html');
$source = '/rhymix/foo/bar//../hello/world\\..';
$target = '/rhymix/foo/hello';
$this->assertEquals($target, $tmpl->normalizePath($source));
$source = '../foo\\bar/../baz/';
$target = '../foo/baz/';
$this->assertEquals($target, $tmpl->normalizePath($source));
}
}

View file

@ -1,13 +1,13 @@
<?php
class TemplateHandlerTest extends \Codeception\TestCase\Test
class TemplateParserV1Test extends \Codeception\Test\Unit
{
private $baseurl;
private $prefix = '<?php if(!defined("__XE__"))exit;';
private $prefix = '<?php if (!defined("RX_VERSION")) exit();';
public function _before()
{
$this->baseurl = '/' . basename(dirname(dirname(dirname(__DIR__)))) . '/';
$this->baseurl = '/' . basename(dirname(dirname(dirname(dirname(__DIR__))))) . '/';
}
public function testParse()
@ -101,42 +101,42 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
// #include
array(
'<dummy /><!--#include("sample.html")--><div>This is another dummy</div>',
'?><dummy /><?php $__tpl=TemplateHandler::getInstance();echo $__tpl->compile(\'tests/unit/classes/template\',\'sample.html\') ?><div>This is another dummy</div>'
'?><dummy /><?php $__tpl=TemplateHandler::getInstance();echo $__tpl->compile(\'tests/_data/template\',\'sample.html\') ?><div>This is another dummy</div>'
),
// <include target="file">
array(
'<dummy /><include target="../sample.html" /><div>This is another dummy</div>',
'?><dummy /><?php $__tpl=TemplateHandler::getInstance();echo $__tpl->compile(\'tests/unit/classes\',\'sample.html\') ?><div>This is another dummy</div>'
'?><dummy /><?php $__tpl=TemplateHandler::getInstance();echo $__tpl->compile(\'tests/_data\',\'sample.html\') ?><div>This is another dummy</div>'
),
// <load target="../../modules/page/lang/lang.xml">
array(
'<dummy /><load target="../../../../modules/page/lang/lang.xml" /><dummy />',
'<dummy /><load target="../../../modules/page/lang/lang.xml" /><dummy />',
'?><dummy /><?php Context::loadLang(\'modules/page/lang\'); ?><dummy />'
),
// <load target="style.css">
array(
'<dummy /><load target="css/style.css" /><dummy />',
'?><dummy /><!--#Meta:tests/unit/classes/template/css/style.css--><?php Context::loadFile([\'tests/unit/classes/template/css/style.css\', \'\', \'\', \'\', []]); ?><dummy />'
'?><dummy /><!--#Meta:tests/_data/template/css/style.css--><?php Context::loadFile([\'tests/_data/template/css/style.css\', \'\', \'\', \'\', []]); ?><dummy />'
),
// <unload target="style.css">
array(
'<dummy /><unload target="css/style.css" /><dummy />',
'?><dummy /><?php Context::unloadFile(\'tests/unit/classes/template/css/style.css\', \'\', \'\'); ?><dummy />'
'?><dummy /><?php Context::unloadFile(\'tests/_data/template/css/style.css\', \'\', \'\'); ?><dummy />'
),
// <!--%import("../../modules/page/tpl/filter/insert_config.xml")-->
array(
'<dummy /><!--%import("../../../../modules/page/tpl/filter/insert_config.xml")--><dummy />',
'<dummy /><!--%import("../../../modules/page/tpl/filter/insert_config.xml")--><dummy />',
'?><dummy /><?php require_once(\'./classes/xml/XmlJsFilter.class.php\');$__xmlFilter=new XmlJsFilter(\'modules/page/tpl/filter\',\'insert_config.xml\');$__xmlFilter->compile(); ?><dummy />'
),
// <!--%import("../script.js",type="body")-->
array(
'<dummy /><!--%import("../script.js",type="body")--><dummy />',
'?><dummy /><!--#Meta:tests/unit/classes/script.js--><?php Context::loadFile([\'tests/unit/classes/script.js\', \'body\', \'\', \'\']); ?><dummy />'
'?><dummy /><!--#Meta:tests/_data/script.js--><?php Context::loadFile([\'tests/_data/script.js\', \'body\', \'\', \'\']); ?><dummy />'
),
// <!--%unload("../script.js",type="body")-->
array(
'<dummy /><!--%unload("../script.js",type="body")--><dummy />',
'?><dummy /><?php Context::unloadFile(\'tests/unit/classes/script.js\', \'\'); ?><dummy />'
'?><dummy /><?php Context::unloadFile(\'tests/_data/script.js\', \'\'); ?><dummy />'
),
// comment
array(
@ -151,11 +151,11 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
// relative path1
array(
'<img src="http://naver.com/naver.gif"><input type="image" src="../local.gif" />',
'?><img src="http://naver.com/naver.gif"><input type="image" src="' . $this->baseurl . 'tests/unit/classes/local.gif" />'
'?><img src="http://naver.com/naver.gif"><input type="image" src="' . $this->baseurl . 'tests/_data/local.gif" />'
),
// relative path2
array(
'<img src="http://naver.com/naver.gif"><input type="image" src="../../../dir/local.gif" />',
'<img src="http://naver.com/naver.gif"><input type="image" src="../../dir/local.gif" />',
'?><img src="http://naver.com/naver.gif"><input type="image" src="' . $this->baseurl . 'tests/dir/local.gif" />'
),
// error case
@ -211,12 +211,12 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
// issue 512 - ignores <marquee>
array(
'<div class="topimgContex"><marquee direction="up" scrollamount="1" height="130" loop="infinity" behavior="lscro">{$lang->sl_show_topimgtext}</marquee></div>',
'?><div class="topimgContex"><marquee direction="up" scrollamount="1" height="130" loop="infinity" behavior="lscro"><?php echo $lang->sl_show_topimgtext ?></marquee></div>'
'?><div class="topimgContex"><marquee direction="up" scrollamount="1" height="130" loop="infinity" behavior="lscro"><?php echo $__Context->lang->sl_show_topimgtext ?? \'\' ?></marquee></div>'
),
// issue 584
array(
'<img cond="$oBodex->display_extra_images[\'mobile\'] && $arr_extra && $arr_extra->bodex->mobile" src="./images/common/mobile.gif" title="mobile" alt="mobile" />',
'if($__Context->oBodex->display_extra_images[\'mobile\'] && $__Context->arr_extra && $__Context->arr_extra->bodex->mobile){ ?><img src="' . $this->baseurl . 'tests/unit/classes/template/images/common/mobile.gif" title="mobile" alt="mobile" /><?php } ?>'
'if($__Context->oBodex->display_extra_images[\'mobile\'] && $__Context->arr_extra && $__Context->arr_extra->bodex->mobile){ ?><img src="' . $this->baseurl . 'tests/_data/template/images/common/mobile.gif" title="mobile" alt="mobile" /><?php } ?>'
),
// issue 831
array(
@ -226,7 +226,7 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
// issue 746
array(
'<img src="../whatever/img.png" />',
'?><img src="' . $this->baseurl . 'tests/unit/classes/whatever/img.png" />'
'?><img src="' . $this->baseurl . 'tests/_data/whatever/img.png" />'
),
// issue 696
array(
@ -236,35 +236,35 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
// https://github.com/xpressengine/xe-core/issues/1510
array(
'<img cond="$foo->bar" src="../common/mobile.gif" />',
'if($__Context->foo->bar ?? false){ ?><img src="' . $this->baseurl . 'tests/unit/classes/common/mobile.gif" /><?php } ?>'
'if($__Context->foo->bar ?? false){ ?><img src="' . $this->baseurl . 'tests/_data/common/mobile.gif" /><?php } ?>'
),
// https://github.com/xpressengine/xe-core/issues/1510
array(
'<img cond="$foo->bar > 100" alt="a!@#$%^&*()_-=[]{}?/" src="../common/mobile.gif" />',
'if($__Context->foo->bar > 100){ ?><img alt="a!@#$%^&*()_-=[]{}?/" src="' . $this->baseurl . 'tests/unit/classes/common/mobile.gif" /><?php } ?>'
'if($__Context->foo->bar > 100){ ?><img alt="a!@#$%^&*()_-=[]{}?/" src="' . $this->baseurl . 'tests/_data/common/mobile.gif" /><?php } ?>'
),
// https://github.com/xpressengine/xe-core/issues/1510
array(
'<img src="../common/mobile.gif" cond="$foo->bar" />',
'if($__Context->foo->bar ?? false){ ?><img src="' . $this->baseurl . 'tests/unit/classes/common/mobile.gif" /><?php } ?>'
'if($__Context->foo->bar ?? false){ ?><img src="' . $this->baseurl . 'tests/_data/common/mobile.gif" /><?php } ?>'
),
// https://github.com/xpressengine/xe-core/issues/1510
array(
'<img class="tmp_class" cond="!$module_info->title" src="../img/common/blank.gif" />',
'if(!$__Context->module_info->title){ ?><img class="tmp_class" src="' . $this->baseurl . 'tests/unit/classes/img/common/blank.gif" /><?php } ?>'
'if(!$__Context->module_info->title){ ?><img class="tmp_class" src="' . $this->baseurl . 'tests/_data/img/common/blank.gif" /><?php } ?>'
),
// https://github.com/xpressengine/xe-core/issues/1510
array(
'<img cond="$mi->title" class="tmp_class"|cond="$mi->use" src="../img/common/blank.gif" />',
'if($__Context->mi->title ?? false){ ?><img<?php if($__Context->mi->use){ ?> class="tmp_class"<?php } ?> src="' . $this->baseurl . 'tests/unit/classes/img/common/blank.gif" /><?php } ?>'
'if($__Context->mi->title ?? false){ ?><img<?php if($__Context->mi->use){ ?> class="tmp_class"<?php } ?> src="' . $this->baseurl . 'tests/_data/img/common/blank.gif" /><?php } ?>'
),
array(
'<input foo="bar" /> <img cond="$foo->bar" alt="alt" src="../common/mobile.gif" />',
'?><input foo="bar" /> <?php if($__Context->foo->bar ?? false){ ?><img alt="alt" src="' . $this->baseurl . 'tests/unit/classes/common/mobile.gif" /><?php } ?>'
'?><input foo="bar" /> <?php if($__Context->foo->bar ?? false){ ?><img alt="alt" src="' . $this->baseurl . 'tests/_data/common/mobile.gif" /><?php } ?>'
),
array(
'<input foo="bar" />' . "\r\n" . '<input foo="bar" /> <img cond="$foo->bar" alt="alt" src="../common/mobile.gif" />',
'?><input foo="bar" />' . "\n" . '<input foo="bar" /> <?php if($__Context->foo->bar ?? false){ ?><img alt="alt" src="' . $this->baseurl . 'tests/unit/classes/common/mobile.gif" /><?php } ?>'
'?><input foo="bar" />' . "\n" . '<input foo="bar" /> <?php if($__Context->foo->bar ?? false){ ?><img alt="alt" src="' . $this->baseurl . 'tests/_data/common/mobile.gif" /><?php } ?>'
),
array(
'asf <img src="{$foo->bar}" />',
@ -272,11 +272,11 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
),
array(
'<img alt="" '.PHP_EOL.' src="../whatever/img.png" />',
'?><img alt="" '.PHP_EOL.' src="' . $this->baseurl . 'tests/unit/classes/whatever/img.png" />'
'?><img alt="" '.PHP_EOL.' src="' . $this->baseurl . 'tests/_data/whatever/img.png" />'
),
array(
'<input>asdf src="../img/img.gif" asdf</input> <img alt="src" src="../whatever/img.png" /> <input>asdf src="../img/img.gif" asdf</input>',
'?><input>asdf src="../img/img.gif" asdf</input> <img alt="src" src="' . $this->baseurl . 'tests/unit/classes/whatever/img.png" /> <input>asdf src="../img/img.gif" asdf</input>'
'?><input>asdf src="../img/img.gif" asdf</input> <img alt="src" src="' . $this->baseurl . 'tests/_data/whatever/img.png" /> <input>asdf src="../img/img.gif" asdf</input>'
),
array(
'<input>asdf src="../img/img.gif" asdf</input>',
@ -289,7 +289,7 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
// srcset (PR #1544)
array(
'<img src="./img/sticker_banner_960w.png" alt="this is a test image." srcset="https://abc.com/static/img/test@2x.png 2x, http://abc.com/static/test@2.5x.png 2.5x,../img/test@3x.png 3x, ../img/test_960w.png 960w, {$mid}/image.png 480w">',
'?><img src="' . $this->baseurl . 'tests/unit/classes/template/img/sticker_banner_960w.png" alt="this is a test image." srcset="https://abc.com/static/img/test@2x.png 2x, http://abc.com/static/test@2.5x.png 2.5x, ' . $this->baseurl . 'tests/unit/classes/img/test@3x.png 3x, ' . $this->baseurl . 'tests/unit/classes/img/test_960w.png 960w, <?php echo $__Context->mid ?? \'\' ?>/image.png 480w">'
'?><img src="' . $this->baseurl . 'tests/_data/template/img/sticker_banner_960w.png" alt="this is a test image." srcset="https://abc.com/static/img/test@2x.png 2x, http://abc.com/static/test@2.5x.png 2.5x, ' . $this->baseurl . 'tests/_data/img/test@3x.png 3x, ' . $this->baseurl . 'tests/_data/img/test_960w.png 960w, <?php echo $__Context->mid ?? \'\' ?>/image.png 480w">'
),
// Rhymix improvements (PR #604)
array(
@ -504,8 +504,7 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
foreach ($tests as $test)
{
$tmpl = new TemplateHandlerWrapper;
$tmpl->init(__DIR__ . '/template', 'no_file.html');
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html');
$result = $tmpl->parse($test[0]);
$between = str_starts_with($test[1], '?>') ? '' : ' ';
$this->assertEquals($this->prefix . $between . $test[1], $result);
@ -514,36 +513,17 @@ class TemplateHandlerTest extends \Codeception\TestCase\Test
public function testParseNoContent()
{
$tmpl = new TemplateHandlerWrapper;
$tmpl->init(__DIR__ . '/template', 'no_file.html');
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html');
$result = $tmpl->parse(null);
$this->assertEquals('', $result);
}
public function testCompileDirect()
{
$tmpl = TemplateHandler::getInstance();
$result = $tmpl->compileDirect(__DIR__ . '/template', 'sample.html');
$tmpl = new \Rhymix\Framework\Template();
$result = $tmpl->compileDirect('./tests/_data/template', 'v1example.html');
$result = trim($result);
$this->assertEquals($this->prefix . ' if($__Context->has_blog ?? false){ ?><a href="http://mygony.com">Taggon\'s blog</a><?php } ?>'.PHP_EOL.'<!--#Meta://external.host/js.js--><?php Context::loadFile([\'//external.host/js.js\', \'\', \'tests\', \'\']); ?>', $result);
}
}
class TemplateHandlerWrapper extends \TemplateHandler {
private $inst;
function __construct() {
$this->inst = parent::getInstance();
}
public function init($tpl_path, $tpl_filename, $tpl_file = '') {
call_user_func(array($this->inst, 'init'), $tpl_path, $tpl_filename, $tpl_file);
}
public function parse($buff = null) {
return call_user_func(array($this->inst, 'parse'), $buff);
}
}

File diff suppressed because it is too large Load diff