Improvement of auto-escaping (#1030)

* Evolution of auto-escaping: no double-escaping when using the 'escape' modifier; add the 'force' mode to the 'escape' modifier; add the 'raw' modifier.
* Add 'raw' modifier's documentation
---------

Co-authored-by: Simon Wisselink <s.wisselink@iwink.nl>
This commit is contained in:
Amaury Bouchard
2024-06-30 13:25:30 +02:00
committed by GitHub
parent 3cb3585432
commit 2289fa69f1
12 changed files with 165 additions and 6 deletions

1
changelog/1030.md Normal file
View File

@ -0,0 +1 @@
- Improvement of auto-escaping [#1030](https://github.com/smarty-php/smarty/pull/1030)

View File

@ -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

View File

@ -73,6 +73,6 @@ This snippet is useful for emails, but see also
<a href="mailto:{$EmailAddress|escape:'hex'}">{$EmailAddress|escape:'mail'}</a>
```
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.

View File

@ -0,0 +1,8 @@
# raw
Prevents variable escaping when [auto-escaping](../../api/configuring.md#enabling-auto-escaping) is activated.
## Basic usage
```smarty
{$myVar|raw}
```

View File

@ -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'

View File

@ -24,22 +24,32 @@ class EscapeModifierCompiler extends Base {
}
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("%(?<!\\\\\\\\)\'%", "\\\'", (string)' . $params[ 0 ] . ')';
case 'javascript':
$compiler->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 @@ class EscapeModifierCompiler extends Base {
}
return '$_smarty_tpl->getSmarty()->getModifierCallback(\'escape\')(' . join(', ', $params) . ')';
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Smarty\Compile\Modifier;
use Smarty\Exception;
/**
* Smarty raw modifier plugin
* Type: modifier
* Name: raw
* Purpose: when escaping is enabled by default, generates a raw output of a variable
*
* @author Amaury Bouchard
*/
class RawModifierCompiler extends Base {
public function compile($params, \Smarty\Compiler\Template $compiler) {
$compiler->setRawOutput(true);
return ($params[0]);
}
}

View File

@ -75,7 +75,7 @@ class ModifierCompiler extends Base {
}
}
}
return $output;
return (string)$output;
}
/**

View File

@ -82,12 +82,13 @@ class PrintExpressionCompiler extends Base {
$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 = "<?php echo {$output};?>\n";
$compiler->setRawOutput(false);
}
return $output;
}

View File

@ -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 @@ class Template extends BaseCompiler {
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;
}
}

View File

@ -35,6 +35,7 @@ class DefaultExtension extends Base {
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 @@ class DefaultExtension extends Base {
return $string;
}
}
}

View File

@ -61,4 +61,68 @@ class AutoEscapeTest extends PHPUnit_Smarty
$this->assertEquals("<p>hi</p>", $this->smarty->fetch($tpl));
}
/**
* test autoescape + raw modifier
*/
public function testAutoEscapeRaw() {
$tpl = $this->smarty->createTemplate('eval:{$foo|raw}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("<a@b.c>", $this->smarty->fetch($tpl));
}
/**
* test autoescape + escape modifier = no double-escaping
*/
public function testAutoEscapeNoDoubleEscape() {
$tpl = $this->smarty->createTemplate('eval:{$foo|escape}');
$tpl->assign('foo', '<a@b.c>');
$this->assertEquals("&lt;a@b.c&gt;", $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', '<a@b.c>');
$this->assertEquals("&amp;lt;a@b.c&amp;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', '<BR>');
$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', '<BR>');
$this->assertEquals("&lt;BR&gt;", $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));
}
}