mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-31 04:27:00 +00:00
Merge branch '3.x' into 4.x
* 3.x: Update CHANGELOG Add `html_attr_relaxed` escaping strategy re-add mixed return type Support short-circuiting in null-safe operator chains Add support for renaming variables in object destructuring Update .gitattributes to remove splitsh.json Fix intro for operator precedence table ?
This commit is contained in:
@@ -8,3 +8,4 @@
|
||||
/phpunit.xml.dist export-ignore
|
||||
/phpstan.neon.dist export-ignore
|
||||
/phpstan-baseline.neon export-ignore
|
||||
/splitsh.json export-ignore
|
||||
|
||||
@@ -19,6 +19,7 @@ return (new Config())
|
||||
'phpdoc_to_comment' => ['ignored_tags' => ['var']],
|
||||
'ordered_imports' => true,
|
||||
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
|
||||
'no_superfluous_phpdoc_tags' => ['allow_mixed' => true],
|
||||
])
|
||||
->setRiskyAllowed(true)
|
||||
->setParallelConfig(ParallelConfigFactory::detect())
|
||||
|
||||
+2
-2
@@ -128,8 +128,8 @@ The following options are available:
|
||||
|
||||
* ``autoescape`` *string*
|
||||
|
||||
Sets the default auto-escaping strategy (``name``, ``html``, ``js``, ``css``,
|
||||
``url``, ``html_attr``, or a PHP callback that takes the template "filename"
|
||||
Sets the default auto-escaping strategy (``name``, ``html``, ``js``, ``css``, ``url``,
|
||||
``html_attr``, ``html_attr_relaxed``, or a PHP callback that takes the template "filename"
|
||||
and returns the escaping strategy to use -- the callback cannot be a function
|
||||
name to avoid collision with built-in escaping strategies); set it to
|
||||
``false`` to disable auto-escaping. The ``name`` escaping strategy determines
|
||||
|
||||
@@ -57,6 +57,16 @@ documents:
|
||||
also when used as the value of an HTML attribute **without quotes**
|
||||
(e.g. ``data-attribute={{ some_value }}``).
|
||||
|
||||
* ``html_attr_relaxed``: like ``html_attr``, but **does not** escape the ``@``, ``:``,
|
||||
``[`` and ``]`` characters. You may want to use this in combination with front-end
|
||||
frameworks that use attribute names like ``v-bind:href`` or ``@click``. But, be
|
||||
aware that in some processing contexts like XML, characters like the colon ``:``
|
||||
may have meaning like for XML namespace separation.
|
||||
|
||||
.. versionadded:: 3.24
|
||||
|
||||
The ``html_attr_relaxed`` strategy has been added in 3.23.
|
||||
|
||||
Note that doing contextual escaping in HTML documents is hard and choosing the
|
||||
right escaping strategy depends on a lot of factors. Please, read related
|
||||
documentation like `the OWASP prevention cheat sheet
|
||||
|
||||
+30
-2
@@ -880,7 +880,8 @@ The following operators don't fit into any of the other categories:
|
||||
{{ user.name }}
|
||||
|
||||
The null-safe operator (``?.``) works like the dot operator but returns
|
||||
``null`` instead of throwing an exception when the left operand is ``null``:
|
||||
``null`` instead of throwing an exception when the left operand is ``null``.
|
||||
If the operand is part of a chain, the rest of the chain is skipped:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
@@ -890,6 +891,9 @@ The following operators don't fit into any of the other categories:
|
||||
{{ user?.address?.city }}
|
||||
{# can be chained for safe navigation through potentially null values #}
|
||||
|
||||
{{ user?.address.city }}
|
||||
{# returns null if user is null, the rest of the chain is skipped (address.city is not evaluated) #}
|
||||
|
||||
After the ``.`` or ``?.``, you can use any expression by wrapping it with
|
||||
parenthesis ``()``.
|
||||
|
||||
@@ -1043,7 +1047,7 @@ Twig uses operators to perform various operations within templates.
|
||||
Understanding the precedence of these operators is crucial for writing correct
|
||||
and efficient Twig templates.
|
||||
|
||||
The operator precedence rules are as follows, with the lowest-precedence
|
||||
The operator precedence rules are as follows, with the highest-precedence
|
||||
operators listed first.
|
||||
|
||||
.. include:: operators_precedence.rst
|
||||
@@ -1128,6 +1132,30 @@ or mapping by extracting values based on property/key names:
|
||||
{{ name }} {# user.name #}
|
||||
{{ email }} {# user.email #}
|
||||
|
||||
You can rename variables during destructuring by using the ``key: variable``
|
||||
syntax, where the key is the property to extract and the variable is the name
|
||||
to assign to:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{% do {name: userName, email: userEmail} = user %}
|
||||
|
||||
{{ userName }} {# user.name #}
|
||||
{{ userEmail }} {# user.email #}
|
||||
|
||||
This is especially useful when you need to destructure multiple objects that
|
||||
share the same property names:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{% do {data: product, error: productError} = loadProduct() %}
|
||||
{% do {data: stock, error: stockError} = loadStock() %}
|
||||
|
||||
{{ product }} {# loadProduct().data #}
|
||||
{{ productError }} {# loadProduct().error #}
|
||||
{{ stock }} {# loadStock().data #}
|
||||
{{ stockError }} {# loadStock().error #}
|
||||
|
||||
.. note::
|
||||
|
||||
Object destructuring uses the :ref:`dot operator <dot_operator>` to access
|
||||
|
||||
@@ -23,7 +23,8 @@ use Twig\Node\Node;
|
||||
*/
|
||||
class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
{
|
||||
private array $properties = [];
|
||||
/** @var list<array{property: string, variable: string}> */
|
||||
private array $mappings = [];
|
||||
|
||||
/**
|
||||
* @param ArrayExpression $left The array expression containing object/mapping destructuring properties
|
||||
@@ -38,7 +39,11 @@ class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
if (!$pair['value'] instanceof ContextVariable) {
|
||||
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno);
|
||||
}
|
||||
$this->properties[] = $pair['value']->getAttribute('name');
|
||||
|
||||
$this->mappings[] = [
|
||||
'property' => $pair['key']->getAttribute('value'),
|
||||
'variable' => $pair['value']->getAttribute('name'),
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($left, $right, $lineno);
|
||||
@@ -48,18 +53,18 @@ class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
{
|
||||
$compiler->addDebugInfo($this);
|
||||
$compiler->raw('[');
|
||||
foreach ($this->properties as $i => $property) {
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$compiler->raw('$context[')->repr($property)->raw(']');
|
||||
$compiler->raw('$context[')->repr($mapping['variable'])->raw(']');
|
||||
}
|
||||
$compiler->raw('] = [');
|
||||
foreach ($this->properties as $i => $property) {
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ')->subcompile($this->getNode('right'))->raw(', ')->repr($property)->raw(', [], \\Twig\\Template::ANY_CALL, false, false, false, ')->repr($this->getNode('right')->getTemplateLine())->raw(')');
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ')->subcompile($this->getNode('right'))->raw(', ')->repr($mapping['property'])->raw(', [], \\Twig\\Template::ANY_CALL, false, false, false, ')->repr($this->getNode('right')->getTemplateLine())->raw(')');
|
||||
}
|
||||
$compiler->raw(']');
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$nodes['arguments'] = $arguments;
|
||||
}
|
||||
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe], $lineno);
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe, 'is_short_circuited' => false, 'var_name' => null], $lineno);
|
||||
}
|
||||
|
||||
public function enableDefinedTest(): void
|
||||
@@ -42,7 +42,6 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$env = $compiler->getEnvironment();
|
||||
$arrayAccessSandbox = false;
|
||||
$nullSafe = $this->getAttribute('null_safe');
|
||||
$objectVar = null;
|
||||
|
||||
// optimize array calls
|
||||
if (
|
||||
@@ -91,18 +90,32 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$this->getNode('node')->setAttribute('ignore_strict_check', true);
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
$objectVar = '$'.$compiler->getVarName();
|
||||
if (null === $nullSafeNode = $nullSafe ? $this : null) {
|
||||
$node = $this->getNode('node');
|
||||
while ($node instanceof self) {
|
||||
if ($node->getAttribute('null_safe')) {
|
||||
$nullSafeNode = $node;
|
||||
break;
|
||||
}
|
||||
$node = $node->getNode('node');
|
||||
}
|
||||
}
|
||||
|
||||
$isShortCircuited = false;
|
||||
if (null !== $nullSafeNode && !$nullSafeNode->isShortCircuited()) {
|
||||
$compiler
|
||||
->raw('((null === ('.$objectVar.' = ')
|
||||
->subcompile($this->getNode('node'))
|
||||
->raw('((null === ('.$nullSafeNode->getVarName($compiler).' = ')
|
||||
->subcompile($nullSafeNode->getNode('node'))
|
||||
->raw(')) ? null : ');
|
||||
|
||||
$nullSafeNode->markAsShortCircuited();
|
||||
$isShortCircuited = true;
|
||||
}
|
||||
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
||||
|
||||
if ($nullSafe) {
|
||||
$compiler->raw($objectVar);
|
||||
$compiler->raw($this->getVarName($compiler));
|
||||
} else {
|
||||
$compiler->subcompile($this->getNode('node'));
|
||||
}
|
||||
@@ -141,7 +154,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$compiler->raw(')');
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
if ($isShortCircuited) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
@@ -155,4 +168,23 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$this->changeIgnoreStrictCheck($node->getNode('node'));
|
||||
}
|
||||
}
|
||||
|
||||
private function markAsShortCircuited(): void
|
||||
{
|
||||
$this->setAttribute('is_short_circuited', true);
|
||||
}
|
||||
|
||||
private function isShortCircuited(): bool
|
||||
{
|
||||
return $this->getAttribute('is_short_circuited');
|
||||
}
|
||||
|
||||
private function getVarName(Compiler $compiler): string
|
||||
{
|
||||
if (null === $this->getAttribute('var_name')) {
|
||||
$this->setAttribute('var_name', $compiler->getVarName());
|
||||
}
|
||||
|
||||
return '$'.$this->getAttribute('var_name');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
if (\in_array('html_attr', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
$bucket['value'][] = 'html_attr_relaxed';
|
||||
}
|
||||
|
||||
if (\in_array('html_attr_relaxed', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
}
|
||||
|
||||
return $bucket['value'];
|
||||
|
||||
@@ -124,7 +124,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
}
|
||||
|
||||
$string = (string) $string;
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) {
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'html_attr_relaxed', 'url'], true)) {
|
||||
// we return the input as is (which can be of any type)
|
||||
return $string;
|
||||
}
|
||||
@@ -256,6 +256,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $string;
|
||||
|
||||
case 'html_attr':
|
||||
case 'html_attr_relaxed':
|
||||
if ('UTF-8' !== $charset) {
|
||||
$string = $this->convertEncoding($string, 'UTF-8', $charset);
|
||||
}
|
||||
@@ -264,7 +265,12 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
|
||||
}
|
||||
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', static function ($matches) {
|
||||
$regex = match ($strategy) {
|
||||
'html_attr' => '#[^a-zA-Z0-9,\.\-_]#Su',
|
||||
'html_attr_relaxed' => '#[^a-zA-Z0-9,\.\-_:@\[\]]#Su',
|
||||
};
|
||||
|
||||
$string = preg_replace_callback($regex, static function ($matches) {
|
||||
/**
|
||||
* This function is adapted from code coming from Zend Framework.
|
||||
*
|
||||
@@ -323,7 +329,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $this->escapers[$strategy]($string, $charset);
|
||||
}
|
||||
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers)));
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr', 'html_attr_relaxed'], array_keys($this->escapers)));
|
||||
|
||||
throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Compiler;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
@@ -333,6 +334,49 @@ class ExpressionParserTest extends TestCase
|
||||
['foo' => (object) ['bar' => false]],
|
||||
'',
|
||||
],
|
||||
// short-circuiting
|
||||
[
|
||||
'{{ foo?.bar.baz }}',
|
||||
['foo' => null],
|
||||
'',
|
||||
],
|
||||
[
|
||||
'{{ foo?.bar.baz?.qux.corge }}',
|
||||
['foo' => null],
|
||||
'',
|
||||
],
|
||||
[
|
||||
'{{ foo?.bar.baz?.qux.corge }}',
|
||||
['foo' => (object) ['bar' => (object) ['baz' => null]]],
|
||||
'',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTestForInvalidNullSafeOperatorShortCircuiting
|
||||
*/
|
||||
public function testInvalidNullSafeOperatorShortCircuiting(string $template, array $data, string $expectedMessage)
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(['template' => $template]), ['strict_variables' => true]);
|
||||
|
||||
$this->expectException(RuntimeError::class);
|
||||
$this->expectExceptionMessage($expectedMessage);
|
||||
|
||||
$env->render('template', $data);
|
||||
}
|
||||
|
||||
public static function getTestForInvalidNullSafeOperatorShortCircuiting()
|
||||
{
|
||||
yield [
|
||||
'{{ foo?.bar.baz }}',
|
||||
['foo' => (object) ['bar' => null]],
|
||||
'Impossible to access an attribute ("baz") on a null variable in "template" at line 1.',
|
||||
];
|
||||
yield [
|
||||
'{{ foo?.bar.baz?.qux.corge }}',
|
||||
['foo' => (object) ['bar' => (object) ['baz' => (object) ['qux' => null]]]],
|
||||
'Impossible to access an attribute ("corge") on a null variable in "template" at line 1.',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ Twig supports the "=" operator (assignment)
|
||||
{% do {name, email} = user %}{{ name }} {{ email }}
|
||||
{% do {first_name, last_name} = user_map %}{{ first_name }} {{ last_name }}
|
||||
{% do {name} = user_obj %}{{ name }}
|
||||
|
||||
# Object destructuring with renaming
|
||||
{% do {name: userName, email: userEmail} = user %}{{ userName }} {{ userEmail }}
|
||||
{% do {first_name: first, last_name: last} = user_map %}{{ first }} {{ last }}
|
||||
{% do {name: objName} = user_obj %}{{ objName }}
|
||||
{% do {name: n1} = user %}{% do {name: n2} = user_obj %}{{ n1 }} {{ n2 }}
|
||||
--DATA--
|
||||
return [
|
||||
'user' => (object)['name' => 'Fabien', 'email' => 'fabien@example.com'],
|
||||
@@ -47,3 +53,9 @@ one two null
|
||||
Fabien fabien@example.com
|
||||
Fabien Potencier
|
||||
Fabien
|
||||
|
||||
# Object destructuring with renaming
|
||||
Fabien fabien@example.com
|
||||
Fabien Potencier
|
||||
Fabien
|
||||
Fabien Fabien
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
--TEST--
|
||||
"escape" filter does not additionally apply the html strategy when the html_attr_relaxed strategy has been applied
|
||||
"escape" filter does not additionally apply the html_attr_relaxed strategy when the html_attr strategy has been applied
|
||||
--TEMPLATE--
|
||||
{% autoescape 'html' %}
|
||||
{{ 'v:bind@click="foo"'|escape('html_attr_relaxed') }}
|
||||
{% endautoescape %}
|
||||
{% autoescape 'html_attr_relaxed' %}
|
||||
{{ 'v:bind@click="foo"' | escape('html_attr') }}
|
||||
{% endautoescape %}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
v:bind@click="foo"
|
||||
v:bind@click="foo"
|
||||
@@ -51,6 +51,9 @@ class GetAttrTest extends NodeTestCase
|
||||
|
||||
$expr = new ContextVariable('foo', 1);
|
||||
$attr = new ConstantExpression('bar', 1);
|
||||
$attr2 = new ConstantExpression('baz', 1);
|
||||
$attr3 = new ConstantExpression('qux', 1);
|
||||
$attr4 = new ConstantExpression('corge', 1);
|
||||
$args = new ArrayExpression([], 1);
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1);
|
||||
@@ -59,6 +62,16 @@ class GetAttrTest extends NodeTestCase
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true);
|
||||
$tests[] = [$node, '((null === ($_v%s = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().'$_v%s, "bar", arguments: [], lineno: 1))', null, true];
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true);
|
||||
$node = new GetAttrExpression($node, $attr2, $args, Template::METHOD_CALL, 1);
|
||||
$tests[] = [$node, '((null === ($_v%s = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v%s, "bar", [], "any", false, false, false, 1), "baz", [], "method", false, false, false, 1))', null, true];
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true);
|
||||
$node = new GetAttrExpression($node, $attr2, $args, Template::ANY_CALL, 1);
|
||||
$node = new GetAttrExpression($node, $attr3, $args, Template::METHOD_CALL, 1, true);
|
||||
$node = new GetAttrExpression($node, $attr4, $args, Template::ANY_CALL, 1);
|
||||
$tests[] = [$node, '((null === ($_v0 = ((null === ($_v1 = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v1, "bar", [], "any", false, false, false, 1), "baz", [], "any", false, false, false, 1)))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v0, "qux", [], "method", false, false, false, 1), "corge", [], "any", false, false, false, 1))', null];
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1);
|
||||
$tests[] = [$node, '(($_v%s = // line 1'."\n".
|
||||
'($context["foo"] ?? null)) && is_array($_v%s) || $_v%s instanceof ArrayAccess ? ($_v%s["bar"] ?? null) : null)', null, true, ];
|
||||
|
||||
@@ -180,6 +180,13 @@ class EscaperRuntimeTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testHtmlAttributeRelaxedEscapingConvertsSpecialChars()
|
||||
{
|
||||
foreach ($this->htmlAttrSpecialChars as $key => $value) {
|
||||
$this->assertEquals($value, (new EscaperRuntime())->escape($key, 'html_attr_relaxed'), 'Failed to escape: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function testJavascriptEscapingConvertsSpecialChars()
|
||||
{
|
||||
foreach ($this->jsSpecialChars as $key => $value) {
|
||||
@@ -331,6 +338,26 @@ class EscaperRuntimeTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testHtmlAttributeRelaxedEscapingEscapesOwaspRecommendedRanges()
|
||||
{
|
||||
$immune = [',', '.', '-', '_', ':', '@', '[', ']']; // Exceptions to escaping ranges
|
||||
for ($chr = 0; $chr < 0xFF; ++$chr) {
|
||||
if ($chr >= 0x30 && $chr <= 0x39
|
||||
|| $chr >= 0x41 && $chr <= 0x5A
|
||||
|| $chr >= 0x61 && $chr <= 0x7A) {
|
||||
$literal = $this->codepointToUtf8($chr);
|
||||
$this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed'));
|
||||
} else {
|
||||
$literal = $this->codepointToUtf8($chr);
|
||||
if (\in_array($literal, $immune)) {
|
||||
$this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed'));
|
||||
} else {
|
||||
$this->assertNotEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr_relaxed'), "$literal should be escaped!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCssEscapingEscapesOwaspRecommendedRanges()
|
||||
{
|
||||
// CSS has no exceptions to escaping ranges
|
||||
|
||||
Reference in New Issue
Block a user