diff --git a/CHANGELOG b/CHANGELOG
index cc3a168deb..8059b09482 100644
--- a/CHANGELOG
+++ b/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
diff --git a/doc/api.rst b/doc/api.rst
index b96b0d4e7f..323ee852bb 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -433,6 +433,24 @@ The escaping rules are implemented as follows:
{% set text = "Twig
" %}
{{ foo ? text|escape : "
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
diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php
index 2e564ebe78..e0119f7370 100644
--- a/src/Extension/CoreExtension.php
+++ b/src/Extension/CoreExtension.php
@@ -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;
diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php
index fc7f6dfeea..d730714102 100644
--- a/src/Extension/EscaperExtension.php
+++ b/src/Extension/EscaperExtension.php
@@ -21,6 +21,10 @@ class EscaperExtension extends AbstractExtension
{
protected $defaultStrategy;
+ private $safeClasses = [];
+
+ private static $safeClassesFrozen = false;
+
/**
* @param string|false|callable $defaultStrategy An escaping strategy
*
@@ -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');
diff --git a/test/Twig/Tests/Extension/CoreTest.php b/test/Twig/Tests/Extension/CoreTest.php
index c29431824a..72c6c846fd 100644
--- a/test/Twig/Tests/Extension/CoreTest.php
+++ b/test/Twig/Tests/Extension/CoreTest.php
@@ -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 [
+ ['<br />', '
', ['\Twig_Tests_Extension_TestClass' => ['js']]],
+ ['
', '\u003Cbr\u0020\/\u003E', ['\Twig_Tests_Extension_TestClass' => ['html']]],
+ ['<br />', '
', ['\Twig_Tests_Extension_SafeHtmlInterface' => ['js']]],
+ ['
', '
', ['\Twig_Tests_Extension_SafeHtmlInterface' => ['all']]],
+ ];
+ }
+
/**
* @dataProvider provideTwigFirstCases
*/
@@ -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 '
';
+ }
+}