From c487c13864f2c835364f03169102ec444723fcd7 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 18 Oct 2023 12:09:45 +0900 Subject: [PATCH] Apply context-aware escape more generally; add can/cannot/canany and env directives --- common/framework/Template.php | 42 ++++++- .../parsers/template/TemplateParser_v2.php | 50 +++++--- tests/_data/template/v2example.compiled.html | 21 ++-- tests/_data/template/v2example.executed.html | 3 +- tests/_data/template/v2example.html | 3 +- .../parsers/TemplateParserV2Test.php | 116 ++++++++++++------ 6 files changed, 165 insertions(+), 70 deletions(-) diff --git a/common/framework/Template.php b/common/framework/Template.php index bf9582449..f4b8ca76b 100644 --- a/common/framework/Template.php +++ b/common/framework/Template.php @@ -37,6 +37,7 @@ class Template */ protected static $_mtime; protected static $_delay_compile; + protected static $_json_options; /** * Provided for compatibility with old TemplateHandler. @@ -76,6 +77,10 @@ class Template { 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) @@ -767,7 +772,42 @@ class Template case 'admin': return $this->user->isAdmin(); case 'manager': return $grant->manager ?? false; case 'member': return $this->user->isMember(); - default: return $grant->$type ?? false; + 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; } } diff --git a/common/framework/parsers/template/TemplateParser_v2.php b/common/framework/parsers/template/TemplateParser_v2.php index 8591cd1c8..a6b9e5624 100644 --- a/common/framework/parsers/template/TemplateParser_v2.php +++ b/common/framework/parsers/template/TemplateParser_v2.php @@ -88,9 +88,13 @@ class TemplateParser_v2 'empty' => ['if (empty(%s)):', 'endif;'], 'admin' => ['if ($this->user->isAdmin()):', 'endif;'], 'auth' => ['if ($this->_v2_checkAuth(%s)):', 'endif;'], + 'can' => ['if ($this->_v2_checkCapability(1, %s)):', 'endif;'], + 'cannot' => ['if ($this->_v2_checkCapability(2, %s)):', 'endif;'], + 'canany' => ['if ($this->_v2_checkCapability(3, %s)):', 'endif;'], 'guest' => ['if (!$this->user->isMember()):', 'endif;'], 'desktop' => ['if (!$__Context->m):', 'endif;'], 'mobile' => ['if ($__Context->m):', 'endif;'], + 'env' => ['if (!empty($_ENV[%s])):', 'endif;'], 'else' => ['else:'], 'elseif' => ['elseif (%s):'], 'case' => ['case %s:'], @@ -665,8 +669,8 @@ class TemplateParser_v2 if ($match[1] === 'json') { return sprintf('config->context === \'JS\' ? ' . - 'json_encode(%s, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT) : ' . - 'htmlspecialchars(json_encode(%s, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT), \ENT_QUOTES, \'UTF-8\', false); ?>', $args, $args); + 'json_encode(%s, self::$_json_options) : ' . + 'htmlspecialchars(json_encode(%s, self::$_json_options), \ENT_QUOTES, \'UTF-8\', false); ?>', $args, $args); } elseif ($match[1] === 'lang') { @@ -726,23 +730,30 @@ class TemplateParser_v2 */ protected function _arrangeOutputFilters(array $match): string { - // Escape is 'autoescape' by default. - $escape_option = 'autoescape'; - // Split content into filters. $filters = array_map('trim', preg_split('#(? '|']); - // Convert variable scope before applying filters. - $str = $this->_escapeCurly($str); - $str = $this->_convertVariableScope($str); + // Set default escape option. + if (preg_match('/^\\$(?:user_)?lang->\\w+$/', $str)) + { + $escape_option = 'autocontext_lang'; + } + else + { + $escape_option = 'autocontext'; + } // Prevent null errors. - if (preg_match('#^\$[\\\\\w\[\]\'":>-]+$#', $str)) + if (preg_match('#^\$[\\\\\w\[\]\'":>-]+$#', $str) && !str_starts_with($str, '$lang->')) { - $str = preg_match('/^\$lang->/', $str) ? $str : "$str ?? ''"; + $str = "$str ?? ''"; } + // Convert variable scope and escape any curly braces. + $str = $this->_escapeCurly($str); + $str = $this->_convertVariableScope($str); + // Apply filters. foreach ($filters as $filter) { @@ -777,8 +788,8 @@ class TemplateParser_v2 $escape_option = 'noescape'; break; case 'json': - $str = "json_encode({$str}, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT)"; - $escape_option = 'autocontext'; + $str = "json_encode({$str}, self::\$_json_options)"; + $escape_option = 'autocontext_json'; break; case 'strip': case 'strip_tags': @@ -797,7 +808,7 @@ class TemplateParser_v2 $str = "strtoupper({$str})"; break; case 'nl2br': - $str = self::_applyEscapeOption($str, $escape_option); + $str = self::_applyEscapeOption($str, $escape_option === 'autocontext' ? 'autoescape' : $escape_option); $str = "nl2br({$str})"; $escape_option = 'noescape'; break; @@ -816,10 +827,10 @@ class TemplateParser_v2 $str = $filter_option ? "number_shorten({$str}, {$filter_option})" : "number_shorten({$str})"; break; case 'link': - $str = self::_applyEscapeOption($str, $escape_option); + $str = self::_applyEscapeOption($str, $escape_option === 'autocontext' ? 'autoescape' : $escape_option); if ($filter_option) { - $filter_option = self::_applyEscapeOption($filter_option, $escape_option); + $filter_option = self::_applyEscapeOption($filter_option, $escape_option === 'autocontext' ? 'autoescape' : $escape_option); $str = "'' . ($str) . ''"; } else @@ -847,14 +858,19 @@ class TemplateParser_v2 */ protected static function _applyEscapeOption(string $str, string $option): string { + $str2 = strtr($str, ["\n" => ' ']); switch($option) { case 'autocontext': - return "\$this->config->context === 'JS' ? ({$str}) : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; + return "\$this->config->context === 'JS' ? escape_js({$str2}) : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; + case 'autocontext_json': + return "\$this->config->context === 'JS' ? {$str2} : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; + case 'autocontext_lang': + return "\$this->config->context === 'JS' ? escape_js({$str2}) : ({$str})"; case 'autoescape': return "htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false)"; case 'autolang': - return "(preg_match('/^\\$(?:user_)?lang->\\w+$/', {$str}) ? ({$str}) : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false))"; + return "(preg_match('/^\\\\\$(?:user_)?lang->\\w+$/', {$str2}) ? ({$str}) : htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', false))"; case 'escape': return "htmlspecialchars({$str}, \ENT_QUOTES, 'UTF-8', true)"; case 'noescape': diff --git a/tests/_data/template/v2example.compiled.html b/tests/_data/template/v2example.compiled.html index 28d4b82eb..b8a1fb282 100644 --- a/tests/_data/template/v2example.compiled.html +++ b/tests/_data/template/v2example.compiled.html @@ -6,7 +6,7 @@ _v2_loadResource('css/style.scss', 'print', '', []); ?> foo = 'FOOFOOFOO'; + $__Context->foo = 'FOOFOO<"FOO">BAR'; ?> bar = ['Rhy', 'miX', 'is', 'da', 'BEST!']; @@ -15,38 +15,38 @@ {{ $foo }} -
+ get('foo')): ?> required="required"> - bar[3] === 'da'): ?> required="required" /> + bar[3] === 'da'): ?> required="required" />
baz))): ?> class="foobar"> foo || $__Context->bar): ?>

Hello bar): ?>foo ?? ''; ?>

-

bar)), \ENT_QUOTES, 'UTF-8', false); ?>

+

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); ?>

-_v2_initLoopVar("RANDOM_LOOP_ID", $__tmp_RANDOM_LOOP_ID); foreach ($__tmp_RANDOM_LOOP_ID as $__Context->k => $__Context->val): ?> +_v2_initLoopVar("64b3371f38fea1", $__tmp_64b3371f38fea1); foreach ($__tmp_64b3371f38fea1 as $__Context->k => $__Context->val): ?>
nosuchvar)): ?> unit tests are cool - k >= 2): ?>class="val ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"> + k >= 2): ?>class="config->context === 'JS' ? escape_js($__Context->val ?? '') : htmlspecialchars($__Context->val ?? '', \ENT_QUOTES, 'UTF-8', false); ?>">
-_v2_incrLoopVar($__loop_RANDOM_LOOP_ID); endforeach; $this->_v2_removeLoopVar($__loop_RANDOM_LOOP_ID); unset($__loop_RANDOM_LOOP_ID); else: ?>
Nothing here...
+_v2_incrLoopVar($__loop_64b3371f38fea1); endforeach; $this->_v2_removeLoopVar($__loop_64b3371f38fea1); unset($__loop_64b3371f38fea1); else: ?>
Nothing here...
_fragments[$__last_fragment_name] = ob_get_flush(); ?> _v2_include("include", $__filename, [(string)$__varname => $__var]); endforeach; })('incl/eachtest', $__Context->bar, 'var'); ?> _v2_include("include", $__filename, [(string)$__varname => $__var]); endforeach; })('incl/eachtest', [], 'anything', 'incl/empty'); ?> m): ?> -

The full class name is , really.

+

The full class name is , config->context === 'JS' ? escape_js(Rhymix\Framework\Push::class) : htmlspecialchars(Rhymix\Framework\Push::class, \ENT_QUOTES, 'UTF-8', false); ?> really.

-
+
_v2_buildAttribute('class', [ 'a-1', 'font-normal' => $__Context->foo, @@ -61,5 +61,6 @@
diff --git a/tests/_data/template/v2example.executed.html b/tests/_data/template/v2example.executed.html index bce0e828b..1baff6af2 100644 --- a/tests/_data/template/v2example.executed.html +++ b/tests/_data/template/v2example.executed.html @@ -17,7 +17,7 @@
-

Hello FOOFOOFOO

+

Hello FOOFOO<"FOO">BAR

RHY|MIX|IS|DA|BEST!

@@ -57,5 +57,6 @@
diff --git a/tests/_data/template/v2example.html b/tests/_data/template/v2example.html index 306b90af9..2b57d75d1 100644 --- a/tests/_data/template/v2example.html +++ b/tests/_data/template/v2example.html @@ -6,7 +6,7 @@ BAR'; ?> @php $bar = ['Rhy', 'miX', 'is', 'da', 'BEST!']; @@ -61,5 +61,6 @@ diff --git a/tests/unit/framework/parsers/TemplateParserV2Test.php b/tests/unit/framework/parsers/TemplateParserV2Test.php index f5b517633..0fc8c750f 100644 --- a/tests/unit/framework/parsers/TemplateParserV2Test.php +++ b/tests/unit/framework/parsers/TemplateParserV2Test.php @@ -234,7 +234,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit { // Basic usage of XE-style single braces $source = '{$var}'; - $target = "var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Single braces with space at beginning will not be parsed @@ -244,22 +244,22 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // Single braces with space at end are OK $source = '{$var }'; - $target = "var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Correct handling of object property and array access $source = '{Context::getRequestVars()->$foo[$bar]}'; - $target = "{\$__Context->foo}[\$__Context->bar], \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar]) : htmlspecialchars(Context::getRequestVars()->{\$__Context->foo}[\$__Context->bar], \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Basic usage of Blade-style double braces $source = '{{ $var }}'; - $target = "var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Double braces without spaces are OK $source = '{{$var}}'; - $target = "var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(\$__Context->var ?? '') : htmlspecialchars(\$__Context->var ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Literal double braces @@ -273,13 +273,13 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $this->assertEquals($target, $this->_parse($source)); // Callback function inside echo statement - $source = '{{ implode("|", array_map(function(\$i) { return \$i + 1; }, $list) }}'; - $target = "list), \ENT_QUOTES, 'UTF-8', false); ?>"; + $source = '{{ implode("|", array_map(function(\$i) { return \$i + 1; }, $list) | noescape }}'; + $target = "list); ?>"; $this->assertEquals($target, $this->_parse($source)); // Multiline echo statement $source = '{{ $foo ?' . "\n" . ' date($foo) :' . "\n" . ' toBool($bar) }}'; - $target = "foo ?\n date(\$__Context->foo) :\n toBool(\$__Context->bar), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "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); ?>"; $this->assertEquals($target, $this->_parse($source)); } @@ -315,9 +315,17 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $target = "foo ?? '', \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); - // Autolang + // Autolang (lang codes are not escaped, but escape_js() is applied in JS context) $source = '{{ $foo|autolang }}'; - $target = "\w+$/', \$__Context->foo ?? '') ? (\$__Context->foo ?? '') : htmlspecialchars(\$__Context->foo ?? '', \ENT_QUOTES, 'UTF-8', false)); ?>"; + $target = "\w+$/', \$__Context->foo ?? '') ? (\$__Context->foo ?? '') : htmlspecialchars(\$__Context->foo ?? '', \ENT_QUOTES, 'UTF-8', false)); ?>"; + $this->assertEquals($target, $this->_parse($source)); + + $source = '{{ $lang->cmd_hello_world }}'; + $target = "config->context === 'JS' ? escape_js(\$__Context->lang->cmd_hello_world) : (\$__Context->lang->cmd_hello_world); ?>"; + $this->assertEquals($target, $this->_parse($source)); + + $source = '{{ $user_lang->user_lang_1234567890 }}'; + $target = "config->context === 'JS' ? escape_js(\$__Context->user_lang->user_lang_1234567890 ?? '') : (\$__Context->user_lang->user_lang_1234567890 ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Escape @@ -344,19 +352,19 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $source = '{{ $foo|json }}'; $target = implode('', [ "config->context === 'JS' ? ", - "(json_encode(\$__Context->foo ?? '', \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT)) : ", - "htmlspecialchars(json_encode(\$__Context->foo ?? '', \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT), \ENT_QUOTES, 'UTF-8', false); ?>", + "json_encode(\$__Context->foo ?? '', self::\$_json_options) : ", + "htmlspecialchars(json_encode(\$__Context->foo ?? '', self::\$_json_options), \ENT_QUOTES, 'UTF-8', false); ?>", ]); $this->assertEquals($target, $this->_parse($source)); // strip_tags $source = '{{ $foo|strip }}'; - $target = "foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(strip_tags(\$__Context->foo ?? '')) : htmlspecialchars(strip_tags(\$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // strip_tags (alternate name) $source = '{{ $foo|upper|strip_tags }}'; - $target = "foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(strip_tags(strtoupper(\$__Context->foo ?? ''))) : htmlspecialchars(strip_tags(strtoupper(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Trim @@ -366,12 +374,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // URL encode $source = '{{ $foo|trim|urlencode }}'; - $target = "foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(rawurlencode(trim(\$__Context->foo ?? ''))) : htmlspecialchars(rawurlencode(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Lowercase $source = '{{ $foo|trim|lower }}'; - $target = "foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(strtolower(trim(\$__Context->foo ?? ''))) : htmlspecialchars(strtolower(trim(\$__Context->foo ?? '')), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Uppercase @@ -391,62 +399,62 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // Array join (default joiner is comma) $source = '{{ $foo|join }}'; - $target = "foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(implode(', ', \$__Context->foo ?? '')) : htmlspecialchars(implode(', ', \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Array join (custom joiner) $source = '{{ $foo|join:"!@!" }}'; - $target = "foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(implode(\"!@!\", \$__Context->foo ?? '')) : htmlspecialchars(implode(\"!@!\", \$__Context->foo ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Date conversion (default format) $source = '{{ $item->regdate | date }}'; - $target = "item->regdate ?? ''), 'Y-m-d H:i:s'), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "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); ?>"; $this->assertEquals($target, $this->_parse($source)); // Date conversion (custom format) $source = "{{ \$item->regdate | date:'n/j H:i' }}"; - $target = "item->regdate ?? ''), 'n/j H:i'), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "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); ?>"; $this->assertEquals($target, $this->_parse($source)); // Date conversion (custom format in variable) $source = "{{ \$item->regdate | date:\$format }}"; - $target = "item->regdate ?? ''), \$__Context->format), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "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); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number format $source = '{{ $num | format }}'; - $target = "num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(number_format(\$__Context->num ?? '')) : htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number format (alternate name) $source = '{{ $num | number_format }}'; - $target = "num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $target = "config->context === 'JS' ? escape_js(number_format(\$__Context->num ?? '')) : htmlspecialchars(number_format(\$__Context->num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number format (custom format) - $source = '{{ $num | number_format:6 }}'; - $target = "num ?? '', '6'), \ENT_QUOTES, 'UTF-8', false); ?>"; + $source = '{{ $num | number_format:6 | noescape }}'; + $target = "num ?? '', '6'); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number format (custom format in variable) - $source = '{{ $num | number_format:$digits }}'; - $target = "num ?? '', \$__Context->digits), \ENT_QUOTES, 'UTF-8', false); ?>"; + $source = '{{ $num | number_format:$digits | noescape }}'; + $target = "num ?? '', \$__Context->digits); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number shorten - $source = '{{ $num | shorten }}'; - $target = "num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $source = '{{ $num | shorten | noescape }}'; + $target = "num ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number shorten (alternate name) - $source = '{{ $num | number_shorten }}'; - $target = "num ?? ''), \ENT_QUOTES, 'UTF-8', false); ?>"; + $source = '{{ $num | number_shorten | noescape }}'; + $target = "num ?? ''); ?>"; $this->assertEquals($target, $this->_parse($source)); // Number shorten (custom format) - $source = '{{ $num | number_shorten:1 }}'; - $target = "num ?? '', '1'), \ENT_QUOTES, 'UTF-8', false); ?>"; + $source = '{{ $num | number_shorten:1 | noescape }}'; + $target = "num ?? '', '1'); ?>"; $this->assertEquals($target, $this->_parse($source)); // Link @@ -489,12 +497,12 @@ class TemplateParserV2Test extends \Codeception\Test\Unit // $lang $source = "{!! \$lang->cmd_yes !!}"; - $target = "lang->cmd_yes ?? ''; ?>"; + $target = "lang->cmd_yes; ?>"; $this->assertEquals($target, $this->_parse($source)); // $loop $source = "{!! \$loop->first !!}"; - $target = "first; ?>"; + $target = "first ?? ''; ?>"; $this->assertEquals($target, $this->_parse($source)); // Escaped dollar sign @@ -731,7 +739,7 @@ class TemplateParserV2Test extends \Codeception\Test\Unit ]); $target = implode("\n", [ "_v2_errorExists('email', 'login')): ?>", - "message ?? '', \ENT_QUOTES, 'UTF-8', false); ?>", + "config->context === 'JS' ? escape_js(\$__Context->message ?? '') : htmlspecialchars(\$__Context->message ?? '', \ENT_QUOTES, 'UTF-8', false); ?>", '', ]); $this->assertEquals($target, $this->_parse($source)); @@ -800,6 +808,34 @@ class TemplateParserV2Test extends \Codeception\Test\Unit '', ]); $this->assertEquals($target, $this->_parse($source)); + + // @can and @cannot, @canany + $source = implode("\n", [ + "@can('foo')", + 'Hello World', + '@endcan', + "", + "@canany(['foo', 'bar'])", + 'Goodbye World', + '', + '' + ]); + $target = implode("\n", [ + '_v2_checkCapability(1, \'foo\')): ?>', + 'Hello World', + '', + '_v2_checkCapability(2, \'bar\')): ?>', + '_v2_checkCapability(3, [\'foo\', \'bar\'])): ?>', + 'Goodbye World', + '', + '', + ]); + $this->assertEquals($target, $this->_parse($source)); + + // @env + $source = "@env('foo') FOO @endenv"; + $target = ' FOO '; + $this->assertEquals($target, $this->_parse($source)); } public function testInlineConditions() @@ -866,8 +902,8 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $source = '@json($var)'; $target = implode('', [ 'config->context === \'JS\' ? ', - 'json_encode($__Context->var, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT) : ', - 'htmlspecialchars(json_encode($__Context->var, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT), \ENT_QUOTES, \'UTF-8\', false); ?>', + 'json_encode($__Context->var, self::$_json_options) : ', + 'htmlspecialchars(json_encode($__Context->var, self::$_json_options), \ENT_QUOTES, \'UTF-8\', false); ?>', ]); $this->assertEquals($target, $this->_parse($source)); @@ -875,8 +911,8 @@ class TemplateParserV2Test extends \Codeception\Test\Unit $source = '@json(["foo" => 1, "bar" => 2])'; $target = implode('', [ 'config->context === \'JS\' ? ', - 'json_encode(["foo" => 1, "bar" => 2], \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT) : ', - 'htmlspecialchars(json_encode(["foo" => 1, "bar" => 2], \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG | \JSON_HEX_QUOT), \ENT_QUOTES, \'UTF-8\', false); ?>', + 'json_encode(["foo" => 1, "bar" => 2], self::$_json_options) : ', + 'htmlspecialchars(json_encode(["foo" => 1, "bar" => 2], self::$_json_options), \ENT_QUOTES, \'UTF-8\', false); ?>', ]); $this->assertEquals($target, $this->_parse($source));