mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-02 18:07:35 +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
|
||||
* 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
|
||||
* 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
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Twig\Extension;
|
||||
use Twig\Environment;
|
||||
use Twig\FileExtensionEscapingStrategy;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\Filter\EscapeFilter;
|
||||
use Twig\Node\Expression\Filter\RawFilter;
|
||||
use Twig\Node\Node;
|
||||
use Twig\NodeVisitor\EscaperNodeVisitor;
|
||||
@@ -51,8 +52,8 @@ final class EscaperExtension extends AbstractExtension
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
|
||||
new TwigFilter('e', [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'], 'node_class' => EscapeFilter::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,
|
||||
'embedded_templates' => $embeddedTemplates,
|
||||
'strategy' => false,
|
||||
'escaper' => false,
|
||||
], 1);
|
||||
|
||||
// populate the template name of all node children
|
||||
@@ -201,8 +202,14 @@ final class ModuleNode extends Node implements CoercesChildrenToStringInterface
|
||||
->write("/**\n")
|
||||
->write(" * @var array<string, MacroNamespace>\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
|
||||
@@ -215,6 +222,10 @@ final class ModuleNode extends Node implements CoercesChildrenToStringInterface
|
||||
->write("\$this->source = \$this->getSourceContext();\n\n")
|
||||
;
|
||||
|
||||
if ($this->getAttribute('escaper')) {
|
||||
$compiler->write("\$this->escaper = \$env->getRuntime('Twig\\Runtime\\EscaperRuntime');\n\n");
|
||||
}
|
||||
|
||||
// parent
|
||||
if (!$this->hasNode('parent')) {
|
||||
$compiler->write("\$this->parent = false;\n\n");
|
||||
|
||||
@@ -18,6 +18,7 @@ use Twig\Node\BlockNode;
|
||||
use Twig\Node\BlockReferenceNode;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\Filter\EscapeFilter;
|
||||
use Twig\Node\Expression\FilterExpression;
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
use Twig\Node\ImportNode;
|
||||
@@ -40,6 +41,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
private $traverser;
|
||||
private $defaultStrategy = false;
|
||||
private $safeVars = [];
|
||||
private bool $usesEscaper = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -55,6 +57,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
$node->setAttribute('strategy', \is_string($this->defaultStrategy) ? $this->defaultStrategy : false);
|
||||
$this->safeVars = [];
|
||||
$this->blocks = [];
|
||||
$this->usesEscaper = false;
|
||||
} elseif ($node instanceof AutoEscapeNode) {
|
||||
$this->statusStack[] = $node->getAttribute('value');
|
||||
} elseif ($node instanceof BlockNode) {
|
||||
@@ -69,10 +72,15 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
public function leaveNode(Node $node, Environment $env): ?Node
|
||||
{
|
||||
if ($node instanceof ModuleNode) {
|
||||
$node->setAttribute('escaper', $this->usesEscaper);
|
||||
$this->defaultStrategy = false;
|
||||
$this->safeVars = [];
|
||||
$this->blocks = [];
|
||||
} elseif ($node instanceof FilterExpression) {
|
||||
if ($node instanceof EscapeFilter) {
|
||||
$this->useTemplateEscaper($node);
|
||||
}
|
||||
|
||||
return $this->preEscapeFilterNode($node, $env);
|
||||
} elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) {
|
||||
$expression = $node->getNode('expr');
|
||||
@@ -175,8 +183,20 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
$line = $node->getTemplateLine();
|
||||
$filter = $env->getFilter('escape');
|
||||
$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
|
||||
|
||||
@@ -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