mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-04 19:07:11 +00:00
Fetch the escaper runtime once in the constructor of templates that escape
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
* Fix `split`, `random`, and `shuffle` merging a trailing newline into the last character of a string
|
* Fix `split`, `random`, and `shuffle` merging a trailing newline into the last character of a string
|
||||||
* Speed up splitting a string into characters in `split`, `random`, and `shuffle`
|
* Speed up splitting a string into characters in `split`, `random`, and `shuffle`
|
||||||
|
* Speed up escaping by fetching the escaper runtime once per template
|
||||||
* Speed up adding extensions to an environment
|
* Speed up adding extensions to an environment
|
||||||
* Fix the escaping safe analysis retaining every compiled template node for the lifetime of the environment
|
* Fix the escaping safe analysis retaining every compiled template node for the lifetime of the environment
|
||||||
* Speed up loading a template that the environment has already loaded
|
* Speed up loading a template that the environment has already loaded
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Twig\Extension;
|
|||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\FileExtensionEscapingStrategy;
|
use Twig\FileExtensionEscapingStrategy;
|
||||||
use Twig\Node\Expression\ConstantExpression;
|
use Twig\Node\Expression\ConstantExpression;
|
||||||
|
use Twig\Node\Expression\Filter\EscapeFilter;
|
||||||
use Twig\Node\Expression\Filter\RawFilter;
|
use Twig\Node\Expression\Filter\RawFilter;
|
||||||
use Twig\Node\Node;
|
use Twig\Node\Node;
|
||||||
use Twig\NodeVisitor\EscaperNodeVisitor;
|
use Twig\NodeVisitor\EscaperNodeVisitor;
|
||||||
@@ -51,8 +52,8 @@ final class EscaperExtension extends AbstractExtension
|
|||||||
public function getFilters(): array
|
public function getFilters(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
|
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe'], 'node_class' => EscapeFilter::class]),
|
||||||
new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
|
new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe'], 'node_class' => EscapeFilter::class]),
|
||||||
new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),
|
new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Twig.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Twig\Node\Expression\Filter;
|
||||||
|
|
||||||
|
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||||
|
use Twig\Compiler;
|
||||||
|
use Twig\Node\Expression\AbstractExpression;
|
||||||
|
use Twig\Node\Expression\ConstantExpression;
|
||||||
|
use Twig\Node\Expression\FilterExpression;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the escaper runtime fetched by the template constructor when the escaper node visitor flagged it.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class EscapeFilter extends FilterExpression
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param AbstractExpression $node
|
||||||
|
*/
|
||||||
|
#[FirstClassTwigCallableReady]
|
||||||
|
public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno)
|
||||||
|
{
|
||||||
|
parent::__construct($node, $filter, $arguments, $lineno);
|
||||||
|
|
||||||
|
$this->setAttribute('template_escaper', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function compileCallable(Compiler $compiler): void
|
||||||
|
{
|
||||||
|
if (!$this->getAttribute('template_escaper')) {
|
||||||
|
parent::compileCallable($compiler);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiler->raw('$this->escaper->escape');
|
||||||
|
$this->compileArguments($compiler);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-1
@@ -74,6 +74,7 @@ final class ModuleNode extends Node implements CoercesChildrenToStringInterface
|
|||||||
'index' => null,
|
'index' => null,
|
||||||
'embedded_templates' => $embeddedTemplates,
|
'embedded_templates' => $embeddedTemplates,
|
||||||
'strategy' => false,
|
'strategy' => false,
|
||||||
|
'escaper' => false,
|
||||||
], 1);
|
], 1);
|
||||||
|
|
||||||
// populate the template name of all node children
|
// populate the template name of all node children
|
||||||
@@ -201,8 +202,14 @@ final class ModuleNode extends Node implements CoercesChildrenToStringInterface
|
|||||||
->write("/**\n")
|
->write("/**\n")
|
||||||
->write(" * @var array<string, MacroNamespace>\n")
|
->write(" * @var array<string, MacroNamespace>\n")
|
||||||
->write(" */\n")
|
->write(" */\n")
|
||||||
->write("private array \$macros = [];\n\n")
|
->write("private array \$macros = [];\n")
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ($this->getAttribute('escaper')) {
|
||||||
|
$compiler->write("private \\Twig\\Runtime\\EscaperRuntime \$escaper;\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiler->raw("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function compileConstructor(Compiler $compiler): void
|
protected function compileConstructor(Compiler $compiler): void
|
||||||
@@ -215,6 +222,10 @@ final class ModuleNode extends Node implements CoercesChildrenToStringInterface
|
|||||||
->write("\$this->source = \$this->getSourceContext();\n\n")
|
->write("\$this->source = \$this->getSourceContext();\n\n")
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ($this->getAttribute('escaper')) {
|
||||||
|
$compiler->write("\$this->escaper = \$env->getRuntime('Twig\\Runtime\\EscaperRuntime');\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
// parent
|
// parent
|
||||||
if (!$this->hasNode('parent')) {
|
if (!$this->hasNode('parent')) {
|
||||||
$compiler->write("\$this->parent = false;\n\n");
|
$compiler->write("\$this->parent = false;\n\n");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use Twig\Node\BlockNode;
|
|||||||
use Twig\Node\BlockReferenceNode;
|
use Twig\Node\BlockReferenceNode;
|
||||||
use Twig\Node\Expression\AbstractExpression;
|
use Twig\Node\Expression\AbstractExpression;
|
||||||
use Twig\Node\Expression\ConstantExpression;
|
use Twig\Node\Expression\ConstantExpression;
|
||||||
|
use Twig\Node\Expression\Filter\EscapeFilter;
|
||||||
use Twig\Node\Expression\FilterExpression;
|
use Twig\Node\Expression\FilterExpression;
|
||||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||||
use Twig\Node\ImportNode;
|
use Twig\Node\ImportNode;
|
||||||
@@ -40,6 +41,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
|||||||
private $traverser;
|
private $traverser;
|
||||||
private $defaultStrategy = false;
|
private $defaultStrategy = false;
|
||||||
private $safeVars = [];
|
private $safeVars = [];
|
||||||
|
private bool $usesEscaper = false;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -55,6 +57,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
|||||||
$node->setAttribute('strategy', \is_string($this->defaultStrategy) ? $this->defaultStrategy : false);
|
$node->setAttribute('strategy', \is_string($this->defaultStrategy) ? $this->defaultStrategy : false);
|
||||||
$this->safeVars = [];
|
$this->safeVars = [];
|
||||||
$this->blocks = [];
|
$this->blocks = [];
|
||||||
|
$this->usesEscaper = false;
|
||||||
} elseif ($node instanceof AutoEscapeNode) {
|
} elseif ($node instanceof AutoEscapeNode) {
|
||||||
$this->statusStack[] = $node->getAttribute('value');
|
$this->statusStack[] = $node->getAttribute('value');
|
||||||
} elseif ($node instanceof BlockNode) {
|
} elseif ($node instanceof BlockNode) {
|
||||||
@@ -69,10 +72,15 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
|||||||
public function leaveNode(Node $node, Environment $env): ?Node
|
public function leaveNode(Node $node, Environment $env): ?Node
|
||||||
{
|
{
|
||||||
if ($node instanceof ModuleNode) {
|
if ($node instanceof ModuleNode) {
|
||||||
|
$node->setAttribute('escaper', $this->usesEscaper);
|
||||||
$this->defaultStrategy = false;
|
$this->defaultStrategy = false;
|
||||||
$this->safeVars = [];
|
$this->safeVars = [];
|
||||||
$this->blocks = [];
|
$this->blocks = [];
|
||||||
} elseif ($node instanceof FilterExpression) {
|
} elseif ($node instanceof FilterExpression) {
|
||||||
|
if ($node instanceof EscapeFilter) {
|
||||||
|
$this->useTemplateEscaper($node);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->preEscapeFilterNode($node, $env);
|
return $this->preEscapeFilterNode($node, $env);
|
||||||
} elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) {
|
} elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) {
|
||||||
$expression = $node->getNode('expr');
|
$expression = $node->getNode('expr');
|
||||||
@@ -175,8 +183,20 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
|||||||
$line = $node->getTemplateLine();
|
$line = $node->getTemplateLine();
|
||||||
$filter = $env->getFilter('escape');
|
$filter = $env->getFilter('escape');
|
||||||
$args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]);
|
$args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]);
|
||||||
|
$class = $filter->getNodeClass();
|
||||||
|
$expression = new $class($node, $filter, $args, $line);
|
||||||
|
|
||||||
return new FilterExpression($node, $filter, $args, $line);
|
if ($expression instanceof EscapeFilter) {
|
||||||
|
$this->useTemplateEscaper($expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function useTemplateEscaper(EscapeFilter $filter): void
|
||||||
|
{
|
||||||
|
$filter->setAttribute('template_escaper', true);
|
||||||
|
$this->usesEscaper = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPriority(): int
|
public function getPriority(): int
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Twig.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Twig\Tests\Node\Expression\Filter;
|
||||||
|
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Loader\ArrayLoader;
|
||||||
|
use Twig\Node\Expression\ConstantExpression;
|
||||||
|
use Twig\Node\Expression\Filter\EscapeFilter;
|
||||||
|
use Twig\Node\Nodes;
|
||||||
|
use Twig\Test\NodeTestCase;
|
||||||
|
|
||||||
|
class EscapeTest extends NodeTestCase
|
||||||
|
{
|
||||||
|
public static function provideTests(): iterable
|
||||||
|
{
|
||||||
|
$env = new Environment(new ArrayLoader());
|
||||||
|
$arguments = new Nodes([new ConstantExpression('html', 1)]);
|
||||||
|
|
||||||
|
$node = new EscapeFilter(new ConstantExpression('foo', 1), $env->getFilter('escape'), $arguments, 1);
|
||||||
|
yield 'not flagged by the escaper node visitor' => [$node, '$this->env->getRuntime(\'Twig\Runtime\EscaperRuntime\')->escape("foo", "html")', $env];
|
||||||
|
|
||||||
|
$node = new EscapeFilter(new ConstantExpression('foo', 1), $env->getFilter('escape'), $arguments, 1);
|
||||||
|
$node->setAttribute('template_escaper', true);
|
||||||
|
yield 'flagged by the escaper node visitor' => [$node, '$this->escaper->escape("foo", "html")', $env];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Twig.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Twig\Tests\NodeVisitor;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\Loader\ArrayLoader;
|
||||||
|
use Twig\Node\Expression\ConstantExpression;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
use Twig\Node\Nodes;
|
||||||
|
use Twig\Node\PrintNode;
|
||||||
|
use Twig\NodeVisitor\NodeVisitorInterface;
|
||||||
|
|
||||||
|
class EscaperTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider provideTemplates
|
||||||
|
*/
|
||||||
|
#[DataProvider('provideTemplates')]
|
||||||
|
public function testTemplatesFetchTheEscaperOnlyWhenTheyEscape(string|false $autoescape, string $template, int $escaperClasses, string $expected): void
|
||||||
|
{
|
||||||
|
$env = new Environment(new ArrayLoader(['index' => $template, 'embedded' => '{% block content %}{% endblock %}']), ['autoescape' => $autoescape]);
|
||||||
|
|
||||||
|
$this->assertSame($escaperClasses, substr_count($env->compileSource($env->getLoader()->getSourceContext('index')), 'private \Twig\Runtime\EscaperRuntime $escaper;'));
|
||||||
|
$this->assertSame($expected, $env->render('index', ['foo' => '<br>']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideTemplates(): iterable
|
||||||
|
{
|
||||||
|
yield 'autoescaped print' => ['html', '{{ foo }}', 1, '<br>'];
|
||||||
|
yield 'print marked as safe' => ['html', '{{ foo|raw }}', 0, '<br>'];
|
||||||
|
yield 'autoescaping disabled' => [false, '{{ foo }}{# not autoescaped #}', 0, '<br>'];
|
||||||
|
yield 'explicit escape filter' => [false, '{{ foo|e }}', 1, '<br>'];
|
||||||
|
yield 'autoescape tag' => [false, '{% autoescape "html" %}{{ foo }}{% endautoescape %}', 1, '<br>'];
|
||||||
|
yield 'escaping in an embedded template only' => [false, '{% embed "embedded" %}{% block content %}{{ foo|e }}{% endblock %}{% endembed %}', 1, '<br>'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEscapeFilterAddedByALaterVisitorStillEscapes(): void
|
||||||
|
{
|
||||||
|
$env = new Environment(new ArrayLoader(['index' => '{{ "<br>" }}']), ['autoescape' => false]);
|
||||||
|
$env->addExtension(new class extends AbstractExtension {
|
||||||
|
public function getNodeVisitors(): array
|
||||||
|
{
|
||||||
|
return [new class implements NodeVisitorInterface {
|
||||||
|
public function enterNode(Node $node, Environment $env): Node
|
||||||
|
{
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaveNode(Node $node, Environment $env): ?Node
|
||||||
|
{
|
||||||
|
if (!$node instanceof PrintNode) {
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filter = $env->getFilter('escape');
|
||||||
|
$class = $filter->getNodeClass();
|
||||||
|
|
||||||
|
return new PrintNode(new $class($node->getNode('expr'), $filter, new Nodes([new ConstantExpression('html', 1)]), 1), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriority(): int
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->assertSame('<br>', $env->render('index'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user