Merge pull request #2531 from kijin/pr/template-v2-fixes

템플릿 v2 문법 일부 변경
This commit is contained in:
Kijin Sung 2025-03-23 20:49:12 +09:00 committed by GitHub
commit e0aaad0d0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 254 additions and 62 deletions

View file

@ -945,6 +945,31 @@ class Template
return count($args) ? in_array((string)$validator_id, $args, true) : true; return count($args) ? in_array((string)$validator_id, $args, true) : true;
} }
/**
* Check if the current visitor is using a mobile device for v2.
*
* @return bool
*/
protected function _v2_isMobile(): bool
{
return UA::isMobile() && (config('mobile.tablets') || !UA::isTablet());
}
/**
* Contextual escape function for v2.
*
* @param string $str
* @return string
*/
protected function _v2_escape(string $str): string
{
switch ($this->config->context)
{
case 'JS': return escape_js($str);
default: return escape($str);
}
}
/** /**
* Lang shortcut for v2. * Lang shortcut for v2.
* *

View file

@ -104,8 +104,8 @@ class TemplateParser_v2
'cannot' => ['if ($this->_v2_checkCapability(2, %s)):', 'endif;'], 'cannot' => ['if ($this->_v2_checkCapability(2, %s)):', 'endif;'],
'canany' => ['if ($this->_v2_checkCapability(3, %s)):', 'endif;'], 'canany' => ['if ($this->_v2_checkCapability(3, %s)):', 'endif;'],
'guest' => ['if (!$this->user->isMember()):', 'endif;'], 'guest' => ['if (!$this->user->isMember()):', 'endif;'],
'desktop' => ["if (!\\Context::get('m')):", 'endif;'], 'desktop' => ['if (!$this->_v2_isMobile()):', 'endif;'],
'mobile' => ["if (\\Context::get('m')):", 'endif;'], 'mobile' => ['if ($this->_v2_isMobile()):', 'endif;'],
'env' => ['if (!empty($_ENV[%s])):', 'endif;'], 'env' => ['if (!empty($_ENV[%s])):', 'endif;'],
'else' => ['else:'], 'else' => ['else:'],
'elseif' => ['elseif (%s):'], 'elseif' => ['elseif (%s):'],
@ -179,20 +179,45 @@ class TemplateParser_v2
*/ */
protected function _addContextSwitches(string $content): string protected function _addContextSwitches(string $content): string
{ {
return preg_replace_callback('#(<script\b([^>]*)|</script)#i', function($match) { // Inline styles.
$content = preg_replace_callback('#(?<=\s)(style=")([^"]*?)"#i', function($match) {
return $match[1] . '<?php $this->config->context = \'CSS\'; ?>' . $match[2] . '<?php $this->config->context = \'HTML\'; ?>"';
}, $content);
// Inline scripts.
$content = preg_replace_callback('#(?<=\s)(href="javascript:|on[a-z]+=")([^"]*?)"#i', function($match) {
return $match[1] . '<?php $this->config->context = \'JS\'; ?>' . $match[2] . '<?php $this->config->context = \'HTML\'; ?>"';
}, $content);
// <style> tags.
$content = preg_replace_callback('#(<style\b([^>]*)|</style)#i', function($match) {
if (substr($match[1], 1, 1) === '/') if (substr($match[1], 1, 1) === '/')
{ {
return '<?php $this->config->context = "HTML"; ?>' . $match[1]; return '<?php $this->config->context = \'HTML\'; ?>' . $match[1];
}
else
{
return $match[1] . '<?php $this->config->context = \'CSS\'; ?>';
}
}, $content);
// <script> tags that aren't links.
$content = preg_replace_callback('#(<script\b([^>]*)|</script)#i', function($match) {
if (substr($match[1], 1, 1) === '/')
{
return '<?php $this->config->context = \'HTML\'; ?>' . $match[1];
} }
elseif (!str_contains($match[2] ?? '', 'src="')) elseif (!str_contains($match[2] ?? '', 'src="'))
{ {
return $match[1] . '<?php $this->config->context = "JS"; ?>'; return $match[1] . '<?php $this->config->context = \'JS\'; ?>';
} }
else else
{ {
return $match[0]; return $match[0];
} }
}, $content); }, $content);
return $content;
} }
/** /**
@ -203,7 +228,7 @@ class TemplateParser_v2
*/ */
protected static function _removeContextSwitches(string $content): string protected static function _removeContextSwitches(string $content): string
{ {
return preg_replace('#<\?php \$this->config->context = "[A-Z]+"; \?>#', '', $content); return preg_replace('#<\?php \$this->config->context = \'[A-Z]+\'; \?>#', '', $content);
} }
/** /**
@ -735,6 +760,7 @@ class TemplateParser_v2
* @dd($var, $var, ...) * @dd($var, $var, ...)
* @stack('name') * @stack('name')
* @url(['mid' => $mid, 'act' => $act]) * @url(['mid' => $mid, 'act' => $act])
* @widget('name', $args)
* *
* @param string $content * @param string $content
* @return string * @return string
@ -748,7 +774,7 @@ class TemplateParser_v2
// Insert JSON, lang codes, and dumps. // Insert JSON, lang codes, and dumps.
$parentheses = self::_getRegexpForParentheses(2); $parentheses = self::_getRegexpForParentheses(2);
$content = preg_replace_callback('#(?<!@)@(json|lang|dump|stack|url)\x20?('. $parentheses . ')#', function($match) { $content = preg_replace_callback('#(?<!@)@(json|lang|dump|dd|stack|url|widget)\x20?('. $parentheses . ')#', function($match) {
$args = self::_convertVariableScope(substr($match[2], 1, -1)); $args = self::_convertVariableScope(substr($match[2], 1, -1));
switch ($match[1]) switch ($match[1])
{ {
@ -757,7 +783,7 @@ class TemplateParser_v2
'json_encode(%s, self::$_json_options2) : ' . 'json_encode(%s, self::$_json_options2) : ' .
'htmlspecialchars(json_encode(%s, self::$_json_options), \ENT_QUOTES, \'UTF-8\', false); ?>', $args, $args); 'htmlspecialchars(json_encode(%s, self::$_json_options), \ENT_QUOTES, \'UTF-8\', false); ?>', $args, $args);
case 'lang': case 'lang':
return sprintf('<?php echo $this->config->context === \'JS\' ? escape_js($this->_v2_lang(%s)) : $this->_v2_lang(%s); ?>', $args, $args); return sprintf('<?php echo $this->config->context === \'HTML\' ? $this->_v2_lang(%s) : $this->_v2_escape($this->_v2_lang(%s)); ?>', $args, $args);
case 'dump': case 'dump':
return sprintf('<?php ob_start(); var_dump(%s); \$__dump = ob_get_clean(); echo rtrim(\$__dump); ?>', $args); return sprintf('<?php ob_start(); var_dump(%s); \$__dump = ob_get_clean(); echo rtrim(\$__dump); ?>', $args);
case 'dd': case 'dd':
@ -765,7 +791,9 @@ class TemplateParser_v2
case 'stack': case 'stack':
return sprintf('<?php echo implode("\n", self::\$_stacks[%s] ?? []) . "\n"; ?>', $args); return sprintf('<?php echo implode("\n", self::\$_stacks[%s] ?? []) . "\n"; ?>', $args);
case 'url': case 'url':
return sprintf('<?php echo $this->config->context === \'JS\' ? escape_js(getNotEncodedUrl(%s)) : getUrl(%s); ?>', $args, $args); return sprintf('<?php echo $this->config->context === \'HTML\' ? getUrl(%s) : $this->_v2_escape(getNotEncodedUrl(%s)); ?>', $args, $args);
case 'widget':
return sprintf('<?php echo \WidgetController::getInstance()->execute(%s); ?>', $args);
default: default:
return $match[0]; return $match[0];
} }
@ -797,6 +825,15 @@ class TemplateParser_v2
return $this->_arrangeOutputFilters($match); return $this->_arrangeOutputFilters($match);
}, $content); }, $content);
// Exclude {single} curly braces in non-HTML contexts.
$content = preg_replace_callback('#(<\?php \$this->config->context = \'(?:CSS|JS)\'; \?>)(.*?)(<\?php \$this->config->context = \'HTML\'; \?>)#s', function($match) {
$match[2] = preg_replace_callback('#(?<!\{)\{(?!\s)([^{}]+?)\}#', function($m) {
$warning = preg_match('#^\$\w#', $m[1]) ? '<?php trigger_error("Template v1 syntax not allowed in CSS/JS context", \E_USER_WARNING); ?>' : '';
return '&#x1B;&#x7B;' . $warning . $m[1] . '&#x1B;&#x7D;';
}, $match[2]);
return $match[1] . $match[2] . $match[3];
}, $content);
// Convert {single} curly braces. // Convert {single} curly braces.
$content = preg_replace_callback('#(?<!\{)\{(?!\s)([^{}]+?)\}#', [$this, '_arrangeOutputFilters'], $content); $content = preg_replace_callback('#(?<!\{)\{(?!\s)([^{}]+?)\}#', [$this, '_arrangeOutputFilters'], $content);
@ -943,11 +980,11 @@ class TemplateParser_v2
switch($option) switch($option)
{ {
case 'autocontext': case 'autocontext':
return "\$this->config->context === 'JS' ? escape_js({$str2}) : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; return "\$this->config->context === 'HTML' ? htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape({$str2})";
case 'autocontext_json': case 'autocontext_json':
return "\$this->config->context === 'JS' ? {$str2} : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; return "\$this->config->context === 'JS' ? {$str2} : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)";
case 'autocontext_lang': case 'autocontext_lang':
return "\$this->config->context === 'JS' ? escape_js({$str2}) : ({$str})"; return "\$this->config->context === 'HTML' ? ({$str}) : \$this->_v2_escape({$str2})";
case 'autoescape': case 'autoescape':
return "htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; return "htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)";
case 'autolang': case 'autolang':

View file

@ -205,7 +205,7 @@ function escape($str, bool $double_escape = true, bool $except_lang_code = false
*/ */
function escape_css(string $str): string function escape_css(string $str): string
{ {
return preg_replace('/[^a-zA-Z0-9_.#\/-]/', '', (string)$str); return preg_replace('/[^a-zA-Z0-9_.,#%\/\'()\x20-]/', '', (string)$str);
} }
/** /**

View file

@ -450,14 +450,21 @@ class WidgetController extends Widget
// Save for debug run-time widget // Save for debug run-time widget
$start = microtime(true); $start = microtime(true);
// urldecode the value of args haejum // Type juggling
$object_vars = get_object_vars($args); if (is_array($args))
if(count($object_vars))
{ {
foreach($object_vars as $key => $val) $args = (object)$args;
}
// Apply urldecode for backward compatibility
if ($escaped)
{
foreach (get_object_vars($args) ?: [] as $key => $val)
{ {
if(in_array($key, array('widgetbox_content','body','class','style','widget_sequence','widget','widget_padding_left','widget_padding_top','widget_padding_bottom','widget_padding_right','widgetstyle','document_srl'))) continue; if (!in_array($key, ['body', 'class', 'style', 'document_srl', 'widget', 'widget_sequence', 'widgetstyle', 'widgetbox_content', 'widget_padding_left', 'widget_padding_top', 'widget_padding_bottom', 'widget_padding_right']))
if($escaped) $args->{$key} = utf8RawUrlDecode($val); {
$args->{$key} = utf8RawUrlDecode($val);
}
} }
} }

View file

@ -0,0 +1,26 @@
<a href="javascript:alert('Hello \u003C\u0022world\u0022\u003E (\u0027string\u0027) variable.jpg')">
Hello &lt;&quot;world&quot;&gt; (&#039;string&#039;) variable.jpg</p>
<p onclick="location.href = 'Hello \u003C\u0022world\u0022\u003E (\u0027string\u0027) variable.jpg';">
<span style="font: Hello &lt;&quot;world&quot;&gt; (&#039;string&#039;) variable.jpg">
Hello &lt;&quot;world&quot;&gt; (&#039;string&#039;) variable.jpg </span>
</p>
<script type="text/javascript">
const str = "Hello \u003C\u0022world\u0022\u003E ('string') variable.jpg";
const tpl = `template literal with ${var} inside`;
const fun = function() {
const foo = 'Hello \u003C\u0022world\u0022\u003E (\u0027string\u0027) variable.jpg';
const bar = 'Hello <"world"> ('string') variable.jpg';
}
</script>
<style>
body{background-image: url('Hello &lt;&quot;world&quot;&gt; (&#039;string&#039;) variable.jpg')}
</style>
<ul class="test">
<li>Hello &lt;&quot;world&quot;&gt; (&#039;string&#039;) variable.jpg</li>
<li>Hello <"world"> ('string') variable.jpg</li>
</ul>

View file

@ -0,0 +1,29 @@
@version(2)
<a href="javascript:alert('{{ $var }}')">
{{ $var }}
</p>
<p onclick="location.href = '{{ $var }}';">
<span style="font: {{ $var }}">
{{ $var }}
</span>
</p>
<script type="text/javascript">
const str = @json($var);
const tpl = `template literal with ${var} inside`;
const fun = function() {
const foo = '{{ $var }}';
const bar = '{{ $var|noescape }}';
}
</script>
<style>
body{background-image: url('{{ $var }}')}
</style>
<ul class="test">
<li>{{ $var }}</li>
<li>{{ $var|noescape }}</li>
</ul>

View file

@ -15,16 +15,16 @@
{{ $foo }} {{ $foo }}
<form action="<?php echo $this->config->context === 'JS' ? escape_js(\RX_BASEURL) : htmlspecialchars(\RX_BASEURL, \ENT_QUOTES, 'UTF-8', false); ?>" method="post"> <form action="<?php echo $this->config->context === 'HTML' ? htmlspecialchars(\RX_BASEURL, \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape(\RX_BASEURL); ?>" method="post">
<input type="hidden" name="_rx_csrf_token" value="<?php echo \Rhymix\Framework\Session::getGenericToken(); ?>" /> <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"<?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; ?> /> <input type="text" value="<?php echo $this->config->context === 'HTML' ? htmlspecialchars($__Context->bar[0] ?? '', \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape($__Context->bar[0] ?? ''); ?>"<?php if ($__Context->bar[3] === 'da'): ?> required="required"<?php endif; ?> />
</form> </form>
<div<?php if (!(isset($__Context->baz))): ?> class="foobar"<?php endif; ?>> <div<?php if (!(isset($__Context->baz))): ?> class="foobar"<?php endif; ?>>
<?php if ($__Context->foo || $__Context->bar): ?> <?php if ($__Context->foo || $__Context->bar): ?>
<p>Hello <?php if ($__Context->bar): ?><?php echo $__Context->foo ?? ''; ?><?php endif; ?></p> <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> <p><?php echo $this->config->context === 'HTML' ? htmlspecialchars(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar)), \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape(implode('|', array_map(function($i) { return strtoupper($i); }, $__Context->bar))); ?></p>
<?php endif; ?> <?php endif; ?>
</div> </div>
@ -33,7 +33,7 @@
<div> <div>
<?php if (empty($__Context->nosuchvar)): ?> <?php if (empty($__Context->nosuchvar)): ?>
<img src="/rhymix/tests/_data/template/bar/rhymix.svg" alt="unit tests are cool" /> <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> <span <?php if ($__Context->k >= 2): ?>class="<?php echo $this->config->context === 'HTML' ? htmlspecialchars($__Context->val ?? '', \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape($__Context->val ?? ''); ?>"<?php endif; ?>></span>
<?php endif; ?> <?php endif; ?>
</div> </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->_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; ?>
@ -42,8 +42,8 @@
<?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', $__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 (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')): ?> <?php if (!$this->_v2_isMobile()): ?>
<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> <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 === 'HTML' ? htmlspecialchars(Rhymix\Framework\Push::class, \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape(Rhymix\Framework\Push::class); ?> really.</p>
<?php endif; ?> <?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); ?>"> <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); ?>">
@ -60,7 +60,11 @@
]); ?>></span> ]); ?>></span>
</div> </div>
<script type="text/javascript"<?php $this->config->context = "JS"; ?>> <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 foo = '<?php echo $this->config->context === 'HTML' ? htmlspecialchars($__Context->foo ?? '', \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape($__Context->foo ?? ''); ?>';
const bar = <?php echo $this->config->context === 'JS' ? json_encode($__Context->bar, self::$_json_options2) : htmlspecialchars(json_encode($__Context->bar, self::$_json_options), \ENT_QUOTES, 'UTF-8', false); ?>; const bar = <?php echo $this->config->context === 'JS' ? json_encode($__Context->bar, self::$_json_options2) : htmlspecialchars(json_encode($__Context->bar, self::$_json_options), \ENT_QUOTES, 'UTF-8', false); ?>;
<?php $this->config->context = "HTML"; ?></script> <?php $this->config->context = 'HTML'; ?></script>
<style<?php $this->config->context = 'CSS'; ?>>
body { background-color: <?php echo $this->config->context === 'HTML' ? htmlspecialchars('#ffffff', \ENT_QUOTES, 'UTF-8', false) : $this->_v2_escape('#ffffff'); ?>; }
<?php $this->config->context = 'HTML'; ?></style>

View file

@ -60,3 +60,7 @@
const foo = 'FOOFOO\u003C\u0022FOO\u0022\u003EBAR'; const foo = 'FOOFOO\u003C\u0022FOO\u0022\u003EBAR';
const bar = ["Rhy","miX","is","da","BEST!"]; const bar = ["Rhy","miX","is","da","BEST!"];
</script> </script>
<style>
body { background-color: #ffffff; }
</style>

View file

@ -64,3 +64,7 @@
const foo = '{{ $foo }}'; const foo = '{{ $foo }}';
const bar = @json($bar); const bar = @json($bar);
</script> </script>
<style>
body { background-color: {{ '#ffffff' }}; }
</style>

View file

@ -248,11 +248,39 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
} }
public function testContextSwitches()
{
// <script> tag
$source = '<script type="text/javascript"> foobar(); </script>';
$target = '<script type="text/javascript"<?php $this->config->context = \'JS\'; ?>> foobar(); <?php $this->config->context = \'HTML\'; ?></script>';
$this->assertEquals($target, $this->_parse($source, true, false));
// Inline script in link href
$source = '<a href="javascript:void(0)">Hello</a>';
$target = '<a href="javascript:<?php $this->config->context = \'JS\'; ?>void(0)<?php $this->config->context = \'HTML\'; ?>">Hello</a>';
$this->assertEquals($target, $this->_parse($source, true, false));
// Inline script in event handler
$source = '<div class="foo" onClick="bar.barr()">Hello</div>';
$target = '<div class="foo" onClick="<?php $this->config->context = \'JS\'; ?>bar.barr()<?php $this->config->context = \'HTML\'; ?>">Hello</div>';
$this->assertEquals($target, $this->_parse($source, true, false));
// <style> tag
$source = '<style> body { font-size: 16px; } </style>';
$target = '<style<?php $this->config->context = \'CSS\'; ?>> body { font-size: 16px; } <?php $this->config->context = \'HTML\'; ?></style>';
$this->assertEquals($target, $this->_parse($source, true, false));
// Inline style
$source = '<div style="background-color: #ffffff;" class="foobar"><span></span></div>';
$target = '<div style="<?php $this->config->context = \'CSS\'; ?>background-color: #ffffff;<?php $this->config->context = \'HTML\'; ?>" class="foobar"><span></span></div>';
$this->assertEquals($target, $this->_parse($source, true, false));
}
public function testEchoStatements() public function testEchoStatements()
{ {
// Basic usage of XE-style single braces // Basic usage of XE-style single braces
$source = '{$var}'; $source = '{$var}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Single braces with space at beginning will not be parsed // Single braces with space at beginning will not be parsed
@ -262,22 +290,22 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Single braces with space at end are OK // Single braces with space at end are OK
$source = '{$var }'; $source = '{$var }';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Correct handling of object property and array access // Correct handling of object property and array access
$source = '{Context::getRequestVars()->$foo[$bar]}'; $source = '{Context::getRequestVars()->$foo[$bar]}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar]) : htmlspecialchars(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar], \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar], \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar]); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Basic usage of Blade-style double braces // Basic usage of Blade-style double braces
$source = '{{ $var }}'; $source = '{{ $var }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Double braces without spaces are OK // Double braces without spaces are OK
$source = '{{$var}}'; $source = '{{$var}}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->var ?? ''); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Literal double braces // Literal double braces
@ -297,7 +325,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Multiline echo statement // Multiline echo statement
$source = '{{ $foo ?' . "\n" . ' date($foo) :' . "\n" . ' toBool($bar) }}'; $source = '{{ $foo ?' . "\n" . ' date($foo) :' . "\n" . ' toBool($bar) }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->foo ? date(\$__Context->foo) : toBool(\$__Context->bar)) : htmlspecialchars(\$__Context->foo ?\n date(\$__Context->foo) :\n toBool(\$__Context->bar), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(\$__Context->foo ?\n date(\$__Context->foo) :\n toBool(\$__Context->bar), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->foo ? date(\$__Context->foo) : toBool(\$__Context->bar)); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
} }
@ -339,11 +367,11 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
$source = '{{ $lang->cmd_hello_world }}'; $source = '{{ $lang->cmd_hello_world }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->lang->cmd_hello_world) : (\$__Context->lang->cmd_hello_world); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? (\$__Context->lang->cmd_hello_world) : \$this->_v2_escape(\$__Context->lang->cmd_hello_world); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
$source = '{{ $user_lang->user_lang_1234567890 }}'; $source = '{{ $user_lang->user_lang_1234567890 }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->user_lang->user_lang_1234567890 ?? '') : (\$__Context->user_lang->user_lang_1234567890 ?? ''); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? (\$__Context->user_lang->user_lang_1234567890 ?? '') : \$this->_v2_escape(\$__Context->user_lang->user_lang_1234567890 ?? ''); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Escape // Escape
@ -366,11 +394,6 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$target = "<?php echo escape_js(\$__Context->foo ?? ''); ?>"; $target = "<?php echo escape_js(\$__Context->foo ?? ''); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Context-aware escape
$source = '<script type="text/javascript"> foobar(); </script>';
$target = '<script type="text/javascript"<?php $this->config->context = "JS"; ?>> foobar(); <?php $this->config->context = "HTML"; ?></script>';
$this->assertEquals($target, $this->_parse($source));
// JSON using context-aware escape // JSON using context-aware escape
$source = '{{ $foo|json }}'; $source = '{{ $foo|json }}';
$target = implode('', [ $target = implode('', [
@ -382,12 +405,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// strip_tags // strip_tags
$source = '{{ $foo|strip }}'; $source = '{{ $foo|strip }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(strip_tags(\$__Context->foo ?? '')) : htmlspecialchars(strip_tags(\$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(strip_tags(\$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(strip_tags(\$__Context->foo ?? '')); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// strip_tags (alternate name) // strip_tags (alternate name)
$source = '{{ $foo|upper|strip_tags }}'; $source = '{{ $foo|upper|strip_tags }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(strip_tags(strtoupper(\$__Context->foo ?? ''))) : htmlspecialchars(strip_tags(strtoupper(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(strip_tags(strtoupper(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(strip_tags(strtoupper(\$__Context->foo ?? ''))); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Trim // Trim
@ -397,12 +420,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// URL encode // URL encode
$source = '{{ $foo|trim|urlencode }}'; $source = '{{ $foo|trim|urlencode }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(rawurlencode(trim(\$__Context->foo ?? ''))) : htmlspecialchars(rawurlencode(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(rawurlencode(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(rawurlencode(trim(\$__Context->foo ?? ''))); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Lowercase // Lowercase
$source = '{{ $foo|trim|lower }}'; $source = '{{ $foo|trim|lower }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(strtolower(trim(\$__Context->foo ?? ''))) : htmlspecialchars(strtolower(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(strtolower(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(strtolower(trim(\$__Context->foo ?? ''))); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Uppercase // Uppercase
@ -422,37 +445,37 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Array join (default joiner is comma) // Array join (default joiner is comma)
$source = '{{ $foo|join }}'; $source = '{{ $foo|join }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(implode(', ', \$__Context->foo ?? '')) : htmlspecialchars(implode(', ', \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(implode(', ', \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(implode(', ', \$__Context->foo ?? '')); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Array join (custom joiner) // Array join (custom joiner)
$source = '{{ $foo|join:"!@!" }}'; $source = '{{ $foo|join:"!@!" }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(implode(\"!@!\", \$__Context->foo ?? '')) : htmlspecialchars(implode(\"!@!\", \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(implode(\"!@!\", \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(implode(\"!@!\", \$__Context->foo ?? '')); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Date conversion (default format) // Date conversion (default format)
$source = '{{ $item->regdate | date }}'; $source = '{{ $item->regdate | date }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s')) : htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s'), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s'), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'Y-m-d H:i:s')); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Date conversion (custom format) // Date conversion (custom format)
$source = "{{ \$item->regdate | date:'n/j H:i' }}"; $source = "{{ \$item->regdate | date:'n/j H:i' }}";
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i')) : htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i'), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i'), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), 'n/j H:i')); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Date conversion (custom format in variable) // Date conversion (custom format in variable)
$source = "{{ \$item->regdate | date:\$format }}"; $source = "{{ \$item->regdate | date:\$format }}";
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format)) : htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(getDisplayDateTime(ztime(\$__Context->item->regdate ?? ''), \$__Context->format)); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Number format // Number format
$source = '{{ $num | format }}'; $source = '{{ $num | format }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(number_format(\$__Context->num ?? '')) : htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(number_format(\$__Context->num ?? '')); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Number format (alternate name) // Number format (alternate name)
$source = '{{ $num | number_format }}'; $source = '{{ $num | number_format }}';
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(number_format(\$__Context->num ?? '')) : htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(number_format(\$__Context->num ?? '')); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Number format (custom format) // Number format (custom format)
@ -573,7 +596,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Script tag with external path // Script tag with external path
$source = '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/js/bootstrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>'; $source = '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/js/bootstrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>';
$target = '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/js/bootstrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"><?php $this->config->context = "HTML"; ?></script>'; $target = '<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0/js/bootstrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>';
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Absolute URL // Absolute URL
@ -797,7 +820,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
]); ]);
$target = implode("\n", [ $target = implode("\n", [
"<?php if (\$this->_v2_errorExists('email', 'login')): ?>", "<?php if (\$this->_v2_errorExists('email', 'login')): ?>",
"<?php echo \$this->config->context === 'JS' ? escape_js(\$__Context->message ?? '') : htmlspecialchars(\$__Context->message ?? '', \ENT_QUOTES, 'UTF-8', false); ?>", "<?php echo \$this->config->context === 'HTML' ? htmlspecialchars(\$__Context->message ?? '', \ENT_QUOTES, 'UTF-8', false) : \$this->_v2_escape(\$__Context->message ?? ''); ?>",
'<?php endif; ?>', '<?php endif; ?>',
]); ]);
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
@ -858,10 +881,10 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
'@endmobile', '@endmobile',
]); ]);
$target = implode("\n", [ $target = implode("\n", [
"<?php if (!\\Context::get('m')): ?>", '<?php if (!$this->_v2_isMobile()): ?>',
'<p>4K or GTFO!</p>', '<p>4K or GTFO!</p>',
'<?php endif; ?>', '<?php endif; ?>',
"<?php if (\\Context::get('m')): ?>", '<?php if ($this->_v2_isMobile()): ?>',
'<p>USB C is the way to go~</p>', '<p>USB C is the way to go~</p>',
'<?php endif; ?>', '<?php endif; ?>',
]); ]);
@ -976,17 +999,17 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// Lang code with variable as name // Lang code with variable as name
$source = '@lang($var->name)'; $source = '@lang($var->name)';
$target = '<?php echo $this->config->context === \'JS\' ? escape_js($this->_v2_lang($__Context->var->name)) : $this->_v2_lang($__Context->var->name); ?>'; $target = '<?php echo $this->config->context === \'HTML\' ? $this->_v2_lang($__Context->var->name) : $this->_v2_escape($this->_v2_lang($__Context->var->name)); ?>';
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Lang code with literal name and variable // Lang code with literal name and variable
$source = "@lang('board.cmd_list_items', \$var)"; $source = "@lang('board.cmd_list_items', \$var)";
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(\$this->_v2_lang('board.cmd_list_items', \$__Context->var)) : \$this->_v2_lang('board.cmd_list_items', \$__Context->var); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? \$this->_v2_lang('board.cmd_list_items', \$__Context->var) : \$this->_v2_escape(\$this->_v2_lang('board.cmd_list_items', \$__Context->var)); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Lang code with class alias // Lang code with class alias
$source = "@use('Rhymix\Framework\Lang', 'Lang')\n" . '<p>@lang(Lang::getLang())</p>'; $source = "@use('Rhymix\Framework\Lang', 'Lang')\n" . '<p>@lang(Lang::getLang())</p>';
$target = "\n" . '<p><?php echo $this->config->context === \'JS\' ? escape_js($this->_v2_lang(Rhymix\Framework\Lang::getLang())) : $this->_v2_lang(Rhymix\Framework\Lang::getLang()); ?></p>'; $target = "\n" . '<p><?php echo $this->config->context === \'HTML\' ? $this->_v2_lang(Rhymix\Framework\Lang::getLang()) : $this->_v2_escape($this->_v2_lang(Rhymix\Framework\Lang::getLang())); ?></p>';
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// Dump one variable // Dump one variable
@ -1001,12 +1024,17 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
// URL // URL
$source = "@url(['mid' => 'foo', 'act' => 'dispBoardWrite'])"; $source = "@url(['mid' => 'foo', 'act' => 'dispBoardWrite'])";
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(getNotEncodedUrl(['mid' => 'foo', 'act' => 'dispBoardWrite'])) : getUrl(['mid' => 'foo', 'act' => 'dispBoardWrite']); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? getUrl(['mid' => 'foo', 'act' => 'dispBoardWrite']) : \$this->_v2_escape(getNotEncodedUrl(['mid' => 'foo', 'act' => 'dispBoardWrite'])); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
// URL old-style with variables // URL old-style with variables
$source = "@url('', 'mid', \$mid, 'act', \$act])"; $source = "@url('', 'mid', \$mid, 'act', \$act])";
$target = "<?php echo \$this->config->context === 'JS' ? escape_js(getNotEncodedUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act])) : getUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act]); ?>"; $target = "<?php echo \$this->config->context === 'HTML' ? getUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act]) : \$this->_v2_escape(getNotEncodedUrl('', 'mid', \$__Context->mid, 'act', \$__Context->act])); ?>";
$this->assertEquals($target, $this->_parse($source));
// Widget
$source = "@widget('login_info', ['skin' => 'default'])";
$target = "<?php echo \WidgetController::getInstance()->execute('login_info', ['skin' => 'default']); ?>";
$this->assertEquals($target, $this->_parse($source)); $this->assertEquals($target, $this->_parse($source));
} }
@ -1197,6 +1225,24 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
$this->assertStringContainsString('/tests/_data/template/css/style.scss', array_first($list)['file']); $this->assertStringContainsString('/tests/_data/template/css/style.scss', array_first($list)['file']);
} }
public function testCompileContextualEscape()
{
// Contextual escape
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'v2contextual.html');
$tmpl->disableCache();
$tmpl->setVars([
'var' => 'Hello <"world"> (\'string\') variable.jpg'
]);
$executed_output = $tmpl->compile();
//Rhymix\Framework\Storage::write(\RX_BASEDIR . 'tests/_data/template/v2contextual.executed.html', $executed_output);
$expected = file_get_contents(\RX_BASEDIR . 'tests/_data/template/v2contextual.executed.html');
$this->assertEquals(
$this->_normalizeWhitespace($expected),
$this->_normalizeWhitespace($executed_output)
);
}
public function testCompileLang() public function testCompileLang()
{ {
// Lang // Lang
@ -1295,9 +1341,10 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
* *
* @param string $source * @param string $source
* @param bool $force_v2 Disable version detection * @param bool $force_v2 Disable version detection
* @param bool $remove_context_switches Remove context switches that make code difficult to read
* @return string * @return string
*/ */
protected function _parse(string $source, bool $force_v2 = true): string protected function _parse(string $source, bool $force_v2 = true, bool $remove_context_switches = true): string
{ {
$tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html'); $tmpl = new \Rhymix\Framework\Template('./tests/_data/template', 'empty.html');
if ($force_v2) if ($force_v2)
@ -1309,6 +1356,13 @@ class TemplateParserV2Test extends \Codeception\Test\Unit
{ {
$result = substr($result, strlen($this->prefix)); $result = substr($result, strlen($this->prefix));
} }
// Remove context switches.
if ($remove_context_switches)
{
$result = preg_replace('#<\?php \$this->config->context = \'[A-Z]+\'; \?>#', '', $result);
}
return $result; return $result;
} }

View file

@ -50,8 +50,10 @@ class FunctionsTest extends \Codeception\Test\Unit
$this->assertEquals('$user_lang-&gt;userLang1234567890', escape('$user_lang->userLang1234567890', true, false)); $this->assertEquals('$user_lang-&gt;userLang1234567890', escape('$user_lang->userLang1234567890', true, false));
$this->assertEquals('$user_lang->userLang1234567890', escape('$user_lang->userLang1234567890', true, true)); $this->assertEquals('$user_lang->userLang1234567890', escape('$user_lang->userLang1234567890', true, true));
$this->assertEquals('expressionalertXSS', escape_css('expression:alert("XSS")')); $this->assertEquals('expressionalert(XSS)', escape_css('expression:alert("XSS")'));
$this->assertEquals('#123456', escape_css('#123456')); $this->assertEquals('#123456', escape_css('#123456'));
$this->assertEquals('16px/160% Segoe UI, sans-serif font-style', escape_css('16px/160% Segoe UI, sans-serif; font-style'));
$this->assertEquals('box-shadow(0 1px 2px rgba(0, 0, 0, 0.15)', escape_css('box-shadow(0 1px 2px rgba(0, 0, 0, "0.15")'));
$this->assertEquals('hello\\\\world', escape_js('hello\\world')); $this->assertEquals('hello\\\\world', escape_js('hello\\world'));
$this->assertEquals('\u003Cbr \/\u003E', escape_js('<br />')); $this->assertEquals('\u003Cbr \/\u003E', escape_js('<br />'));