Skip to content

Commit

Permalink
Improvement of auto-escaping (#1030)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Amaury and wisskid authored Jun 30, 2024
1 parent 3cb3585 commit 2289fa6
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 6 deletions.
1 change: 1 addition & 0 deletions changelog/1030.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Improvement of auto-escaping [#1030](https://github.com/smarty-php/smarty/pull/1030)
29 changes: 29 additions & 0 deletions docs/api/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/designers/language-modifiers/language-modifier-escape.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 8 additions & 0 deletions docs/designers/language-modifiers/language-modifier-raw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# raw

Prevents variable escaping when [auto-escaping](../../api/configuring.md#enabling-auto-escaping) is activated.

## Basic usage
```smarty
{$myVar|raw}
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 11 additions & 1 deletion src/Compile/Modifier/EscapeModifierCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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("%(?<!\\\\\\\\)\'%", "\\\'", (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)' .
Expand All @@ -53,4 +63,4 @@ public function compile($params, \Smarty\Compiler\Template $compiler) {
}
return '$_smarty_tpl->getSmarty()->getModifierCallback(\'escape\')(' . join(', ', $params) . ')';
}
}
}
21 changes: 21 additions & 0 deletions src/Compile/Modifier/RawModifierCompiler.php
Original file line number Diff line number Diff line change
@@ -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]);
}
}
2 changes: 1 addition & 1 deletion src/Compile/ModifierCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function compile($args, \Smarty\Compiler\Template $compiler, $parameter =
}
}
}
return $output;
return (string)$output;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Compile/PrintExpressionCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<?php echo {$output};?>\n";
$compiler->setRawOutput(false);
}
return $output;
}
Expand Down
23 changes: 23 additions & 0 deletions src/Compiler/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
3 changes: 2 additions & 1 deletion src/Extension/DefaultExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -753,4 +754,4 @@ public function smarty_modifier_truncate($string, $length = 80, $etc = '...', $b
return $string;
}

}
}
64 changes: 64 additions & 0 deletions tests/UnitTests/A_Core/AutoEscape/AutoEscapeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,68 @@ function ($params, $content) { return $content == null ? null : "<p>".$content.
$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));
}

}

0 comments on commit 2289fa6

Please sign in to comment.