Skip to content

Commit

Permalink
added the possibility to register classes/interfaces as being safe fo…
Browse files Browse the repository at this point in the history
…r the escaper
  • Loading branch information
fabpot committed May 20, 2019
1 parent 571929f commit 6b06269
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
@@ -1,5 +1,6 @@
* 1.42.0 (2019-XX-XX)

* added the possibility to register classes/interfaces as being safe for the escaper ("EscaperExtension::addSafeClass()")
* fixed the "filter" filter when the argument is \Traversable but does not implement \Iterator (\SimpleXmlElement for instance)
* fixed a PHP fatal error when calling a macro imported in a block in a nested block
* fixed a PHP fatal error when calling a macro imported in the template in another macro
Expand Down
18 changes: 18 additions & 0 deletions doc/api.rst
Expand Up @@ -433,6 +433,24 @@ The escaping rules are implemented as follows:
{% set text = "Twig<br />" %}
{{ foo ? text|escape : "<br />Twig" }} {# the result of the expression won't be escaped #}
* Objects with a ``__toString`` method are converted to strings and
escaped. You can mark some classes and/or interfaces as being safe for some
strategies via ``EscaperExtension::addSafeClass()``:

.. code-block:: twig
// mark object of class Foo as safe for the HTML strategy
$escaper->addSafeClass('Foo', ['html']);
// mark object of interface Foo as safe for the HTML strategy
$escaper->addSafeClass('FooInterface', ['html']);
// mark object of class Foo as safe for the HTML and JS strategies
$escaper->addSafeClass('Foo', ['html', 'js']);
// mark object of class Foo as safe for all strategies
$escaper->addSafeClass('Foo', ['all']);
* Escaping is applied before printing, after any other filter is applied:

.. code-block:: twig
Expand Down
32 changes: 32 additions & 0 deletions src/Extension/CoreExtension.php
Expand Up @@ -1022,6 +1022,38 @@ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $char

if (!\is_string($string)) {
if (\is_object($string) && method_exists($string, '__toString')) {
if ($autoescape) {
static $safeLookup;
static $safeClasses;

if (null === $safeLookup) {
$safeLookup = [];
$safeClasses = $env->getExtension('Twig\Extension\EscaperExtension')->getSafeClasses();
foreach ($safeClasses as $class => $strategies) {
foreach ($strategies as $s) {
$safeLookup[$s][$class] = true;
}
}
}

$c = get_class($string);
if (!isset($safeClasses[$c])) {
$safeClasses[$c] = [];
foreach (class_parents($string) + class_implements($string) as $class) {
if (isset($safeClasses[$class])) {
$safeClasses[$c] = array_unique(array_merge($safeClasses[$c], $safeClasses[$class]));
foreach ($safeClasses[$class] as $s) {
$safeLookup[$s][$c] = true;
}
}
}
}

if (isset($safeLookup[$strategy][$c]) || isset($safeLookup['all'][$c])) {
return (string) $string;
}
}

$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
return $string;
Expand Down
39 changes: 39 additions & 0 deletions src/Extension/EscaperExtension.php
Expand Up @@ -21,6 +21,10 @@ class EscaperExtension extends AbstractExtension
{
protected $defaultStrategy;

private $safeClasses = [];

private static $safeClassesFrozen = false;

/**
* @param string|false|callable $defaultStrategy An escaping strategy
*
Expand Down Expand Up @@ -100,6 +104,41 @@ public function getName()
{
return 'escaper';
}

/**
* @internal
*/
public function getSafeClasses(): array
{
self::$safeClassesFrozen = true;

return $this->safeClasses;
}

public function setSafeClasses(array $safeClasses = []): void
{
if (self::$safeClassesFrozen) {
throw new \LogicException('Unable to set the safe classes for escaping as twig_escape_filter() has already been initialized.');
}

foreach ($safeClasses as $class => $strategies) {
$this->addSafeClass($class, $strategies);
}
}

public function addSafeClass(string $class, array $strategies): void
{
if (self::$safeClassesFrozen) {
throw new \LogicException('Unable to set the safe classes for escaping as twig_escape_filter() has already been initialized.');
}

$class = ltrim($class, '\\');
if (!isset($this->safeClasses[$class])) {
$this->safeClasses[$class] = [];
}

$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
}
}

class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
Expand Down
37 changes: 37 additions & 0 deletions test/Twig/Tests/Extension/CoreTest.php
Expand Up @@ -159,6 +159,31 @@ public function testUnknownCustomEscaper()
twig_escape_filter(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()), 'foo', 'bar');
}

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
* @dataProvider provideObjectsForEscaping
*/
public function testObjectEscaping(string $escapedHtml, string $escapedJs, array $safeClasses)
{
$obj = new Twig_Tests_Extension_TestClass();

$twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
$twig->getExtension('\Twig\Extension\EscaperExtension')->setSafeClasses($safeClasses);
$this->assertSame($escapedHtml, twig_escape_filter($twig, $obj, 'html', null, true));
$this->assertSame($escapedJs, twig_escape_filter($twig, $obj, 'js', null, true));
}

public function provideObjectsForEscaping()
{
return [
['&lt;br /&gt;', '<br />', ['\Twig_Tests_Extension_TestClass' => ['js']]],
['<br />', '\u003Cbr\u0020\/\u003E', ['\Twig_Tests_Extension_TestClass' => ['html']]],
['&lt;br /&gt;', '<br />', ['\Twig_Tests_Extension_SafeHtmlInterface' => ['js']]],
['<br />', '<br />', ['\Twig_Tests_Extension_SafeHtmlInterface' => ['all']]],
];
}

/**
* @dataProvider provideTwigFirstCases
*/
Expand Down Expand Up @@ -369,3 +394,15 @@ public function valid()
return isset($this->arrayKeys[$this->position]);
}
}

interface Twig_Tests_Extension_SafeHtmlInterface
{
}

class Twig_Tests_Extension_TestClass implements Twig_Tests_Extension_SafeHtmlInterface
{
public function __toString()
{
return '<br />';
}
}

0 comments on commit 6b06269

Please sign in to comment.