diff --git a/CHANGELOG b/CHANGELOG
index 908bc3005..977cfb9c9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
* 2.11.0 (2019-XX-XX)
+ * added the possibility to register classes/interfaces as being safe for the escaper ("EscaperExtension::addSafeClass()")
* deprecated CoreExtension::setEscaper() and CoreExtension::getEscapers() in favor of the same methods on EscaperExtension
* macros are now auto-imported in the template they are defined (under the ``_self`` variable)
* added support for macros on "is defined" tests
diff --git a/doc/api.rst b/doc/api.rst
index 0f0abcee9..5c0294c20 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -415,6 +415,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/EscaperExtension.php b/src/Extension/EscaperExtension.php
index 497cfde20..2ccab8b2f 100644
--- a/src/Extension/EscaperExtension.php
+++ b/src/Extension/EscaperExtension.php
@@ -14,11 +14,17 @@ use Twig\FileExtensionEscapingStrategy;
use Twig\NodeVisitor\EscaperNodeVisitor;
use Twig\TokenParser\AutoEscapeTokenParser;
use Twig\TwigFilter;
+use Twig\Environment;
+use Twig\Error\RuntimeError;
+use Twig\Extension\CoreExtension;
+use Twig\Markup;
final class EscaperExtension extends AbstractExtension
{
private $defaultStrategy;
private $escapers = [];
+ private $safeClasses = [];
+ private $safeLookup = [];
/**
* @param string|false|callable $defaultStrategy An escaping strategy
@@ -43,8 +49,8 @@ final class EscaperExtension extends AbstractExtension
public function getFilters()
{
return [
- new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
- new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
+ new TwigFilter('escape', [$this, 'escape'], ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
+ new TwigFilter('e', [$this, 'escape'], ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
];
}
@@ -104,6 +110,278 @@ final class EscaperExtension extends AbstractExtension
{
return $this->escapers;
}
+
+ public function setSafeClasses(array $safeClasses = [])
+ {
+ $this->safeClasses = [];
+ $this->safeLookup = [];
+ foreach ($safeClasses as $class => $strategies) {
+ $this->addSafeClass($class, $strategies);
+ }
+ }
+
+ public function addSafeClass(string $class, array $strategies)
+ {
+ $class = ltrim($class, '\\');
+ if (!isset($this->safeClasses[$class])) {
+ $this->safeClasses[$class] = [];
+ }
+ $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
+
+ foreach ($strategies as $strategy) {
+ $this->safeLookup[$strategy][$class] = true;
+ }
+ }
+
+ /**
+ * Escapes a string.
+ *
+ * @param mixed $string The value to be escaped
+ * @param string $strategy The escaping strategy
+ * @param string $charset The charset
+ * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
+ *
+ * @return string
+ */
+ function escape(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
+ {
+ if ($autoescape && $string instanceof Markup) {
+ return $string;
+ }
+
+ if (!\is_string($string)) {
+ if (\is_object($string) && method_exists($string, '__toString')) {
+ if ($autoescape) {
+ $c = get_class($string);
+ if (!isset($this->safeClasses[$c])) {
+ $this->safeClasses[$c] = [];
+ foreach (class_parents($string) + class_implements($string) as $class) {
+ if (isset($this->safeClasses[$class])) {
+ $this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class]));
+ foreach ($this->safeClasses[$class] as $s) {
+ $this->safeLookup[$s][$c] = true;
+ }
+ }
+ }
+ }
+ if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) {
+ return (string) $string;
+ }
+ }
+
+ $string = (string) $string;
+ } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
+ return $string;
+ }
+ }
+
+ if ('' === $string) {
+ return '';
+ }
+
+ if (null === $charset) {
+ $charset = $env->getCharset();
+ }
+
+ switch ($strategy) {
+ case 'html':
+ // see https://secure.php.net/htmlspecialchars
+
+ // Using a static variable to avoid initializing the array
+ // each time the function is called. Moving the declaration on the
+ // top of the function slow downs other escaping strategies.
+ static $htmlspecialcharsCharsets = [
+ 'ISO-8859-1' => true, 'ISO8859-1' => true,
+ 'ISO-8859-15' => true, 'ISO8859-15' => true,
+ 'utf-8' => true, 'UTF-8' => true,
+ 'CP866' => true, 'IBM866' => true, '866' => true,
+ 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
+ '1251' => true,
+ 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
+ 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
+ 'BIG5' => true, '950' => true,
+ 'GB2312' => true, '936' => true,
+ 'BIG5-HKSCS' => true,
+ 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
+ 'EUC-JP' => true, 'EUCJP' => true,
+ 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
+ ];
+
+ if (isset($htmlspecialcharsCharsets[$charset])) {
+ return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
+ }
+
+ if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
+ // cache the lowercase variant for future iterations
+ $htmlspecialcharsCharsets[$charset] = true;
+
+ return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
+ }
+
+ $string = iconv($charset, 'UTF-8', $string);
+ $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+
+ return iconv('UTF-8', $charset, $string);
+
+ case 'js':
+ // escape all non-alphanumeric characters
+ // into their \x or \uHHHH representations
+ if ('UTF-8' !== $charset) {
+ $string = iconv($charset, 'UTF-8', $string);
+ }
+
+ if (!preg_match('//u', $string)) {
+ throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
+ }
+
+ $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
+ $char = $matches[0];
+
+ /*
+ * A few characters have short escape sequences in JSON and JavaScript.
+ * Escape sequences supported only by JavaScript, not JSON, are ommitted.
+ * \" is also supported but omitted, because the resulting string is not HTML safe.
+ */
+ static $shortMap = [
+ '\\' => '\\\\',
+ '/' => '\\/',
+ "\x08" => '\b',
+ "\x0C" => '\f',
+ "\x0A" => '\n',
+ "\x0D" => '\r',
+ "\x09" => '\t',
+ ];
+
+ if (isset($shortMap[$char])) {
+ return $shortMap[$char];
+ }
+
+ // \uHHHH
+ $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
+ $char = strtoupper(bin2hex($char));
+
+ if (4 >= \strlen($char)) {
+ return sprintf('\u%04s', $char);
+ }
+
+ return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
+ }, $string);
+
+ if ('UTF-8' !== $charset) {
+ $string = iconv('UTF-8', $charset, $string);
+ }
+
+ return $string;
+
+ case 'css':
+ if ('UTF-8' !== $charset) {
+ $string = iconv($charset, 'UTF-8', $string);
+ }
+
+ if (!preg_match('//u', $string)) {
+ throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
+ }
+
+ $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
+ $char = $matches[0];
+
+ return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
+ }, $string);
+
+ if ('UTF-8' !== $charset) {
+ $string = iconv('UTF-8', $charset, $string);
+ }
+
+ return $string;
+
+ case 'html_attr':
+ if ('UTF-8' !== $charset) {
+ $string = iconv($charset, 'UTF-8', $string);
+ }
+
+ if (!preg_match('//u', $string)) {
+ throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
+ }
+
+ $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
+ /**
+ * This function is adapted from code coming from Zend Framework.
+ *
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
+ * @license https://framework.zend.com/license/new-bsd New BSD License
+ */
+ $chr = $matches[0];
+ $ord = \ord($chr);
+
+ /*
+ * The following replaces characters undefined in HTML with the
+ * hex entity for the Unicode replacement character.
+ */
+ if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
+ return '�';
+ }
+
+ /*
+ * Check if the current character to escape has a name entity we should
+ * replace it with while grabbing the hex value of the character.
+ */
+ if (1 === \strlen($chr)) {
+ /*
+ * While HTML supports far more named entities, the lowest common denominator
+ * has become HTML5's XML Serialisation which is restricted to the those named
+ * entities that XML supports. Using HTML entities would result in this error:
+ * XML Parsing Error: undefined entity
+ */
+ static $entityMap = [
+ 34 => '"', /* quotation mark */
+ 38 => '&', /* ampersand */
+ 60 => '<', /* less-than sign */
+ 62 => '>', /* greater-than sign */
+ ];
+
+ if (isset($entityMap[$ord])) {
+ return $entityMap[$ord];
+ }
+
+ return sprintf('%02X;', $ord);
+ }
+
+ /*
+ * Per OWASP recommendations, we'll use hex entities for any other
+ * characters where a named entity does not exist.
+ */
+ return sprintf('%04X;', mb_ord($chr, 'UTF-8'));
+ }, $string);
+
+ if ('UTF-8' !== $charset) {
+ $string = iconv('UTF-8', $charset, $string);
+ }
+
+ return $string;
+
+ case 'url':
+ return rawurlencode($string);
+
+ default:
+ static $escapers;
+
+ if (null === $escapers) {
+ // merge the ones set on CoreExtension for BC (to be removed in 3.0)
+ $escapers = array_merge(
+ $env->getExtension(CoreExtension::class)->getEscapers(false),
+ $env->getExtension(EscaperExtension::class)->getEscapers()
+ );
+ }
+
+ if (isset($escapers[$strategy])) {
+ return $escapers[$strategy]($env, $string, $charset);
+ }
+
+ $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
+
+ throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
+ }
+ }
}
class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
@@ -111,10 +389,7 @@ class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
namespace {
use Twig\Environment;
-use Twig\Error\RuntimeError;
-use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
-use Twig\Markup;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
@@ -131,235 +406,13 @@ function twig_raw_filter($string)
}
/**
- * Escapes a string.
- *
- * @param mixed $string The value to be escaped
- * @param string $strategy The escaping strategy
- * @param string $charset The charset
- * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
- *
- * @return string
+ * @deprecated since Twig 2.11, to be removed in 3.0; use EscaperExtension::escape instead
*/
function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
{
- if ($autoescape && $string instanceof Markup) {
- return $string;
- }
+ @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::escape" instead.', __METHOD__, EscaperExtension::class), E_USER_DEPRECATED);
- if (!\is_string($string)) {
- if (\is_object($string) && method_exists($string, '__toString')) {
- $string = (string) $string;
- } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
- return $string;
- }
- }
-
- if ('' === $string) {
- return '';
- }
-
- if (null === $charset) {
- $charset = $env->getCharset();
- }
-
- switch ($strategy) {
- case 'html':
- // see https://secure.php.net/htmlspecialchars
-
- // Using a static variable to avoid initializing the array
- // each time the function is called. Moving the declaration on the
- // top of the function slow downs other escaping strategies.
- static $htmlspecialcharsCharsets = [
- 'ISO-8859-1' => true, 'ISO8859-1' => true,
- 'ISO-8859-15' => true, 'ISO8859-15' => true,
- 'utf-8' => true, 'UTF-8' => true,
- 'CP866' => true, 'IBM866' => true, '866' => true,
- 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
- '1251' => true,
- 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
- 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
- 'BIG5' => true, '950' => true,
- 'GB2312' => true, '936' => true,
- 'BIG5-HKSCS' => true,
- 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
- 'EUC-JP' => true, 'EUCJP' => true,
- 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
- ];
-
- if (isset($htmlspecialcharsCharsets[$charset])) {
- return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
- }
-
- if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
- // cache the lowercase variant for future iterations
- $htmlspecialcharsCharsets[$charset] = true;
-
- return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
- }
-
- $string = iconv($charset, 'UTF-8', $string);
- $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
-
- return iconv('UTF-8', $charset, $string);
-
- case 'js':
- // escape all non-alphanumeric characters
- // into their \x or \uHHHH representations
- if ('UTF-8' !== $charset) {
- $string = iconv($charset, 'UTF-8', $string);
- }
-
- if (!preg_match('//u', $string)) {
- throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
- }
-
- $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
- $char = $matches[0];
-
- /*
- * A few characters have short escape sequences in JSON and JavaScript.
- * Escape sequences supported only by JavaScript, not JSON, are ommitted.
- * \" is also supported but omitted, because the resulting string is not HTML safe.
- */
- static $shortMap = [
- '\\' => '\\\\',
- '/' => '\\/',
- "\x08" => '\b',
- "\x0C" => '\f',
- "\x0A" => '\n',
- "\x0D" => '\r',
- "\x09" => '\t',
- ];
-
- if (isset($shortMap[$char])) {
- return $shortMap[$char];
- }
-
- // \uHHHH
- $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
- $char = strtoupper(bin2hex($char));
-
- if (4 >= \strlen($char)) {
- return sprintf('\u%04s', $char);
- }
-
- return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
- }, $string);
-
- if ('UTF-8' !== $charset) {
- $string = iconv('UTF-8', $charset, $string);
- }
-
- return $string;
-
- case 'css':
- if ('UTF-8' !== $charset) {
- $string = iconv($charset, 'UTF-8', $string);
- }
-
- if (!preg_match('//u', $string)) {
- throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
- }
-
- $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
- $char = $matches[0];
-
- return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
- }, $string);
-
- if ('UTF-8' !== $charset) {
- $string = iconv('UTF-8', $charset, $string);
- }
-
- return $string;
-
- case 'html_attr':
- if ('UTF-8' !== $charset) {
- $string = iconv($charset, 'UTF-8', $string);
- }
-
- if (!preg_match('//u', $string)) {
- throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
- }
-
- $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
- /**
- * This function is adapted from code coming from Zend Framework.
- *
- * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
- * @license https://framework.zend.com/license/new-bsd New BSD License
- */
- $chr = $matches[0];
- $ord = \ord($chr);
-
- /*
- * The following replaces characters undefined in HTML with the
- * hex entity for the Unicode replacement character.
- */
- if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
- return '�';
- }
-
- /*
- * Check if the current character to escape has a name entity we should
- * replace it with while grabbing the hex value of the character.
- */
- if (1 === \strlen($chr)) {
- /*
- * While HTML supports far more named entities, the lowest common denominator
- * has become HTML5's XML Serialisation which is restricted to the those named
- * entities that XML supports. Using HTML entities would result in this error:
- * XML Parsing Error: undefined entity
- */
- static $entityMap = [
- 34 => '"', /* quotation mark */
- 38 => '&', /* ampersand */
- 60 => '<', /* less-than sign */
- 62 => '>', /* greater-than sign */
- ];
-
- if (isset($entityMap[$ord])) {
- return $entityMap[$ord];
- }
-
- return sprintf('%02X;', $ord);
- }
-
- /*
- * Per OWASP recommendations, we'll use hex entities for any other
- * characters where a named entity does not exist.
- */
- return sprintf('%04X;', mb_ord($chr, 'UTF-8'));
- }, $string);
-
- if ('UTF-8' !== $charset) {
- $string = iconv('UTF-8', $charset, $string);
- }
-
- return $string;
-
- case 'url':
- return rawurlencode($string);
-
- default:
- static $escapers;
-
- if (null === $escapers) {
- // merge the ones set on CoreExtension for BC (to be removed in 3.0)
- $escapers = array_merge(
- $env->getExtension(CoreExtension::class)->getEscapers(false),
- $env->getExtension(EscaperExtension::class)->getEscapers()
- );
- }
-
- if (isset($escapers[$strategy])) {
- return $escapers[$strategy]($env, $string, $charset);
- }
-
- $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
-
- throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
- }
+ return $env->getExtension(EscaperExtension::class)->escape($env, $string, $strategy, $charset, $autoescape);
}
/**
diff --git a/test/Twig/Tests/Extension/EscaperTest.php b/test/Twig/Tests/Extension/EscaperTest.php
index 78c05b1f2..88143d9be 100644
--- a/test/Twig/Tests/Extension/EscaperTest.php
+++ b/test/Twig/Tests/Extension/EscaperTest.php
@@ -158,7 +158,7 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
foreach ($this->htmlSpecialChars as $key => $value) {
- $this->assertEquals($value, twig_escape_filter($twig, $key, 'html'), 'Failed to escape: '.$key);
+ $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'html'), 'Failed to escape: '.$key);
}
}
@@ -166,7 +166,7 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
foreach ($this->htmlAttrSpecialChars as $key => $value) {
- $this->assertEquals($value, twig_escape_filter($twig, $key, 'html_attr'), 'Failed to escape: '.$key);
+ $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'html_attr'), 'Failed to escape: '.$key);
}
}
@@ -174,47 +174,47 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
foreach ($this->jsSpecialChars as $key => $value) {
- $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: '.$key);
+ $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'js'), 'Failed to escape: '.$key);
}
}
public function testJavascriptEscapingReturnsStringIfZeroLength()
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
- $this->assertEquals('', twig_escape_filter($twig, '', 'js'));
+ $this->assertEquals('', $twig->getExtension(EscaperExtension::class)->escape($twig, '', 'js'));
}
public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits()
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
- $this->assertEquals('123', twig_escape_filter($twig, '123', 'js'));
+ $this->assertEquals('123', $twig->getExtension(EscaperExtension::class)->escape($twig, '123', 'js'));
}
public function testCssEscapingConvertsSpecialChars()
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
foreach ($this->cssSpecialChars as $key => $value) {
- $this->assertEquals($value, twig_escape_filter($twig, $key, 'css'), 'Failed to escape: '.$key);
+ $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'css'), 'Failed to escape: '.$key);
}
}
public function testCssEscapingReturnsStringIfZeroLength()
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
- $this->assertEquals('', twig_escape_filter($twig, '', 'css'));
+ $this->assertEquals('', $twig->getExtension(EscaperExtension::class)->escape($twig, '', 'css'));
}
public function testCssEscapingReturnsStringIfContainsOnlyDigits()
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
- $this->assertEquals('123', twig_escape_filter($twig, '123', 'css'));
+ $this->assertEquals('123', $twig->getExtension(EscaperExtension::class)->escape($twig, '123', 'css'));
}
public function testUrlEscapingConvertsSpecialChars()
{
$twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
foreach ($this->urlSpecialChars as $key => $value) {
- $this->assertEquals($value, twig_escape_filter($twig, $key, 'url'), 'Failed to escape: '.$key);
+ $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'url'), 'Failed to escape: '.$key);
}
}
@@ -276,15 +276,15 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
|| $chr >= 0x41 && $chr <= 0x5A
|| $chr >= 0x61 && $chr <= 0x7A) {
$literal = $this->codepointToUtf8($chr);
- $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js'));
+ $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'js'));
} else {
$literal = $this->codepointToUtf8($chr);
if (\in_array($literal, $immune)) {
- $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js'));
+ $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'js'));
} else {
$this->assertNotEquals(
$literal,
- twig_escape_filter($twig, $literal, 'js'),
+ $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'js'),
"$literal should be escaped!");
}
}
@@ -300,15 +300,15 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
|| $chr >= 0x41 && $chr <= 0x5A
|| $chr >= 0x61 && $chr <= 0x7A) {
$literal = $this->codepointToUtf8($chr);
- $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr'));
+ $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'html_attr'));
} else {
$literal = $this->codepointToUtf8($chr);
if (\in_array($literal, $immune)) {
- $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr'));
+ $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'html_attr'));
} else {
$this->assertNotEquals(
$literal,
- twig_escape_filter($twig, $literal, 'html_attr'),
+ $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'html_attr'),
"$literal should be escaped!");
}
}
@@ -324,12 +324,12 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
|| $chr >= 0x41 && $chr <= 0x5A
|| $chr >= 0x61 && $chr <= 0x7A) {
$literal = $this->codepointToUtf8($chr);
- $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'css'));
+ $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'css'));
} else {
$literal = $this->codepointToUtf8($chr);
$this->assertNotEquals(
$literal,
- twig_escape_filter($twig, $literal, 'css'),
+ $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'css'),
"$literal should be escaped!");
}
}
@@ -343,7 +343,7 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
$twig = new Environment($this->getMockBuilder(LoaderInterface::class)->getMock());
$twig->getExtension(EscaperExtension::class)->setEscaper('foo', 'foo_escaper_for_test');
- $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy));
+ $this->assertSame($expected, $twig->getExtension(EscaperExtension::class)->escape($twig, $string, $strategy));
}
public function provideCustomEscaperCases()
@@ -360,7 +360,29 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
*/
public function testUnknownCustomEscaper()
{
- twig_escape_filter(new Environment($this->getMockBuilder(LoaderInterface::class)->getMock()), 'foo', 'bar');
+ $twig = new Environment($this->getMockBuilder(LoaderInterface::class)->getMock());
+ $twig->getExtension(EscaperExtension::class)->escape($twig, 'foo', 'bar');
+ }
+
+ /**
+ * @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->getExtension(EscaperExtension::class)->escape($twig, $obj, 'html', null, true));
+ $this->assertSame($escapedJs, $twig->getExtension(EscaperExtension::class)->escape($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']]],
+ ];
}
}
@@ -368,3 +390,14 @@ function foo_escaper_for_test(Environment $twig, $string, $charset)
{
return $string.$charset;
}
+
+interface Twig_Tests_Extension_SafeHtmlInterface
+{
+}
+class Twig_Tests_Extension_TestClass implements Twig_Tests_Extension_SafeHtmlInterface
+{
+ public function __toString()
+ {
+ return '
';
+ }
+}
diff --git a/test/Twig/Tests/IntegrationTest.php b/test/Twig/Tests/IntegrationTest.php
index 6bd727b1e..90a7099a7 100644
--- a/test/Twig/Tests/IntegrationTest.php
+++ b/test/Twig/Tests/IntegrationTest.php
@@ -22,6 +22,7 @@ use Twig\TokenParser\AbstractTokenParser;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
+use Twig\Extension\EscaperExtension;
// This function is defined to check that escaping strategies
// like html works even if a function with the same name is defined.
@@ -203,7 +204,7 @@ class TwigTestExtension extends AbstractExtension
*/
public function escape_and_nl2br($env, $value, $sep = '
')
{
- return $this->nl2br(twig_escape_filter($env, $value, 'html'), $sep);
+ return $this->nl2br($env->getExtension(EscaperExtension::class)->escape($env, $value, 'html'), $sep);
}
/**