diff --git a/changelog/1030.md b/changelog/1030.md new file mode 100644 index 000000000..f7cba021e --- /dev/null +++ b/changelog/1030.md @@ -0,0 +1 @@ +- Improvement of auto-escaping [#1030](https://github.com/smarty-php/smarty/pull/1030) \ No newline at end of file diff --git a/docs/api/configuring.md b/docs/api/configuring.md index ee2ebf7e9..540f6906d 100644 --- a/docs/api/configuring.md +++ b/docs/api/configuring.md @@ -143,6 +143,35 @@ Enable auto-escaping for HTML as follows: $smarty->setEscapeHtml(true); ``` +When auto-escaping is enabled, the `|escape` modifier's default mode (`html`) has no effect, +to avoid double-escaping. It is possible to force it with the `force` mode. +Other modes (`htmlall`, `url`, `urlpathinfo`, `quotes`, `javascript`) may be used +with the result you might expect, without double-escaping. + +Even when auto-escaping is enabled, you might want to display the content of a variable without +escaping it. To do so, use the `|raw` modifier. + +Examples (with auto-escaping enabled): +```smarty +{* these three statements are identical *} +{$myVar} +{$myVar|escape} +{$myVar|escape:'html'} + +{* no double-escaping on these statements *} +{$var|escape:'htmlall'} +{$myVar|escape:'url'} +{$myVar|escape:'urlpathinfo'} +{$myVar|escape:'quotes'} +{$myVar|escape:'javascript'} + +{* no escaping at all *} +{$myVar|raw} + +{* force double-escaping *} +{$myVar|escape:'force'} +``` + ## Disabling compile check By default, Smarty tests to see if the current template has changed since the last time diff --git a/docs/designers/language-modifiers/language-modifier-escape.md b/docs/designers/language-modifiers/language-modifier-escape.md index 6fd5dd2b4..18c98f1cb 100644 --- a/docs/designers/language-modifiers/language-modifier-escape.md +++ b/docs/designers/language-modifiers/language-modifier-escape.md @@ -73,6 +73,6 @@ This snippet is useful for emails, but see also {$EmailAddress|escape:'mail'} ``` -See also [escaping smarty parsing](../language-basic-syntax/language-escaping.md), +See also [auto-escaping](../../api/configuring.md#enabling-auto-escaping), [escaping smarty parsing](../language-basic-syntax/language-escaping.md), [`{mailto}`](../language-custom-functions/language-function-mailto.md) and the [obfuscating email -addresses](../../appendixes/tips.md#obfuscating-e-mail-addresses) page. +addresses](../../appendixes/tips.md#obfuscating-e-mail-addresses) pages. diff --git a/docs/designers/language-modifiers/language-modifier-raw.md b/docs/designers/language-modifiers/language-modifier-raw.md new file mode 100644 index 000000000..e9cce97d3 --- /dev/null +++ b/docs/designers/language-modifiers/language-modifier-raw.md @@ -0,0 +1,8 @@ +# raw + +Prevents variable escaping when [auto-escaping](../../api/configuring.md#enabling-auto-escaping) is activated. + +## Basic usage +```smarty +{$myVar|raw} +``` diff --git a/mkdocs.yml b/mkdocs.yml index 74ad349ca..4fffe7d96 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,6 +68,7 @@ nav: - 'noprint': 'designers/language-modifiers/language-modifier-noprint.md' - 'number_format': 'designers/language-modifiers/language-modifier-number-format.md' - 'nl2br': 'designers/language-modifiers/language-modifier-nl2br.md' + - 'raw': 'designers/language-modifiers/language-modifier-raw.md' - 'regex_replace': 'designers/language-modifiers/language-modifier-regex-replace.md' - 'replace': 'designers/language-modifiers/language-modifier-replace.md' - 'round': 'designers/language-modifiers/language-modifier-round.md' diff --git a/src/Compile/Modifier/EscapeModifierCompiler.php b/src/Compile/Modifier/EscapeModifierCompiler.php index b600e08c8..4352359f0 100644 --- a/src/Compile/Modifier/EscapeModifierCompiler.php +++ b/src/Compile/Modifier/EscapeModifierCompiler.php @@ -24,22 +24,32 @@ public function compile($params, \Smarty\Compiler\Template $compiler) { } switch ($esc_type) { case 'html': + case 'force': + // in case of auto-escaping, and without the 'force' option, no double-escaping + if ($compiler->getSmarty()->escape_html && $esc_type != 'force') + return $params[0]; + // otherwise, escape the variable return 'htmlspecialchars((string)' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ', ' . var_export($double_encode, true) . ')'; // no break case 'htmlall': + $compiler->setRawOutput(true); return 'htmlentities(mb_convert_encoding((string)' . $params[ 0 ] . ', \'UTF-8\', ' . var_export($char_set, true) . '), ENT_QUOTES, \'UTF-8\', ' . var_export($double_encode, true) . ')'; // no break case 'url': + $compiler->setRawOutput(true); return 'rawurlencode((string)' . $params[ 0 ] . ')'; case 'urlpathinfo': + $compiler->setRawOutput(true); return 'str_replace("%2F", "/", rawurlencode((string)' . $params[ 0 ] . '))'; case 'quotes': + $compiler->setRawOutput(true); // escape unescaped single quotes return 'preg_replace("%(?setRawOutput(true); // escape quotes and backslashes, newlines, etc. // see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements return 'strtr((string)' . @@ -53,4 +63,4 @@ public function compile($params, \Smarty\Compiler\Template $compiler) { } return '$_smarty_tpl->getSmarty()->getModifierCallback(\'escape\')(' . join(', ', $params) . ')'; } -} \ No newline at end of file +} diff --git a/src/Compile/Modifier/RawModifierCompiler.php b/src/Compile/Modifier/RawModifierCompiler.php new file mode 100644 index 000000000..c001bb1d4 --- /dev/null +++ b/src/Compile/Modifier/RawModifierCompiler.php @@ -0,0 +1,21 @@ +setRawOutput(true); + return ($params[0]); + } +} diff --git a/src/Compile/ModifierCompiler.php b/src/Compile/ModifierCompiler.php index 4e6232244..dfec3d777 100644 --- a/src/Compile/ModifierCompiler.php +++ b/src/Compile/ModifierCompiler.php @@ -75,7 +75,7 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = } } } - return $output; + return (string)$output; } /** diff --git a/src/Compile/PrintExpressionCompiler.php b/src/Compile/PrintExpressionCompiler.php index 3302254f9..3642551ee 100644 --- a/src/Compile/PrintExpressionCompiler.php +++ b/src/Compile/PrintExpressionCompiler.php @@ -82,12 +82,13 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter = $output = $compiler->compileModifier($modifierlist, $output); } - if ($compiler->getTemplate()->getSmarty()->escape_html) { + if ($compiler->getTemplate()->getSmarty()->escape_html && !$compiler->isRawOutput()) { $output = "htmlspecialchars((string) ({$output}), ENT_QUOTES, '" . addslashes(\Smarty\Smarty::$_CHARSET) . "')"; } } $output = "\n"; + $compiler->setRawOutput(false); } return $output; } diff --git a/src/Compiler/Template.php b/src/Compiler/Template.php index 1d4aa2447..9b2c1a1f2 100644 --- a/src/Compiler/Template.php +++ b/src/Compiler/Template.php @@ -313,6 +313,12 @@ class Template extends BaseCompiler { */ private $noCacheStackDepth = 0; + /** + * disabled auto-escape (when set to true, the next variable output is not auto-escaped) + * + * @var boolean + */ + private $raw_output = false; /** * Initialize compiler @@ -1486,4 +1492,21 @@ public function isNocacheActive(): bool { public function getTagStack(): array { return $this->_tag_stack; } + + /** + * Should the next variable output be raw (true) or auto-escaped (false) + * @return bool + */ + public function isRawOutput(): bool { + return $this->raw_output; + } + + /** + * Should the next variable output be raw (true) or auto-escaped (false) + * @param bool $raw_output + * @return void + */ + public function setRawOutput(bool $raw_output): void { + $this->raw_output = $raw_output; + } } diff --git a/src/Extension/DefaultExtension.php b/src/Extension/DefaultExtension.php index cecc4a46f..88390b941 100644 --- a/src/Extension/DefaultExtension.php +++ b/src/Extension/DefaultExtension.php @@ -35,6 +35,7 @@ public function getModifierCompiler(string $modifier): ?\Smarty\Compile\Modifier case 'lower': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\LowerModifierCompiler(); break; case 'nl2br': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\Nl2brModifierCompiler(); break; case 'noprint': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\NoPrintModifierCompiler(); break; + case 'raw': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\RawModifierCompiler(); break; case 'round': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\RoundModifierCompiler(); break; case 'str_repeat': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\StrRepeatModifierCompiler(); break; case 'string_format': $this->modifiers[$modifier] = new \Smarty\Compile\Modifier\StringFormatModifierCompiler(); break; @@ -753,4 +754,4 @@ public function smarty_modifier_truncate($string, $length = 80, $etc = '...', $b return $string; } -} \ No newline at end of file +} diff --git a/tests/UnitTests/A_Core/AutoEscape/AutoEscapeTest.php b/tests/UnitTests/A_Core/AutoEscape/AutoEscapeTest.php index f26f0f934..4a4ef0662 100644 --- a/tests/UnitTests/A_Core/AutoEscape/AutoEscapeTest.php +++ b/tests/UnitTests/A_Core/AutoEscape/AutoEscapeTest.php @@ -61,4 +61,68 @@ function ($params, $content) { return $content == null ? null : "

".$content. $this->assertEquals("

hi

", $this->smarty->fetch($tpl)); } + /** + * test autoescape + raw modifier + */ + public function testAutoEscapeRaw() { + $tpl = $this->smarty->createTemplate('eval:{$foo|raw}'); + $tpl->assign('foo', ''); + $this->assertEquals("", $this->smarty->fetch($tpl)); + } + + /** + * test autoescape + escape modifier = no double-escaping + */ + public function testAutoEscapeNoDoubleEscape() { + $tpl = $this->smarty->createTemplate('eval:{$foo|escape}'); + $tpl->assign('foo', ''); + $this->assertEquals("<a@b.c>", $this->smarty->fetch($tpl)); + } + + /** + * test autoescape + escape modifier = force double-escaping + */ + public function testAutoEscapeForceDoubleEscape() { + $tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'force\'}'); + $tpl->assign('foo', ''); + $this->assertEquals("&lt;a@b.c&gt;", $this->smarty->fetch($tpl)); + } + + /** + * test autoescape + escape modifier = special escape + */ + public function testAutoEscapeSpecialEscape() { + $tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'url\'}'); + $tpl->assign('foo', 'aa bb'); + $this->assertEquals("aa%20bb", $this->smarty->fetch($tpl)); + } + + /** + * test autoescape + escape modifier = special escape + */ + public function testAutoEscapeSpecialEscape2() { + $tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'url\'}'); + $tpl->assign('foo', '
'); + $this->assertEquals("%3CBR%3E", $this->smarty->fetch($tpl)); + } + + /** + * test autoescape + escape modifier = special escape + */ + public function testAutoEscapeSpecialEscape3() { + $tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'htmlall\'}'); + $tpl->assign('foo', '
'); + $this->assertEquals("<BR>", $this->smarty->fetch($tpl)); + } + + + /** + * test autoescape + escape modifier = special escape + */ + public function testAutoEscapeSpecialEscape4() { + $tpl = $this->smarty->createTemplate('eval:{$foo|escape:\'javascript\'}'); + $tpl->assign('foo', '<\''); + $this->assertEquals("<\\'", $this->smarty->fetch($tpl)); + } + }