diff --git a/CHANGELOG b/CHANGELOG index 6fe41c6d4..8f423332e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.30.1 (2026-XX-XX) * Reject a deprecated `Template` instance created by another environment in `Environment::resolveTemplate()` + * Fix a compilation error inside an `autoescape` tag leaking its escaping strategy into the next compiled template * Speed up reading object attributes backed by getters or class constants * Fix the sandbox not reporting the line of a rejected `guard` tag diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 6063d8586..4c57a8c12 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -51,12 +51,14 @@ final class EscaperNodeVisitor implements NodeVisitorInterface public function enterNode(Node $node, Environment $env): Node { if ($node instanceof ModuleNode) { + $this->defaultStrategy = false; if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getExtension(EscaperExtension::class)->getDefaultStrategy($node->getTemplateName())) { $this->defaultStrategy = $defaultStrategy; } $node->setAttribute('strategy', \is_string($this->defaultStrategy) ? $this->defaultStrategy : false); $this->safeVars = []; $this->blocks = []; + $this->statusStack = []; $this->usesEscaper = false; } elseif ($node instanceof AutoEscapeNode) { $this->statusStack[] = $node->getAttribute('value'); diff --git a/tests/NodeVisitor/EscaperTest.php b/tests/NodeVisitor/EscaperTest.php index 35e9d071c..4b8f37faf 100644 --- a/tests/NodeVisitor/EscaperTest.php +++ b/tests/NodeVisitor/EscaperTest.php @@ -14,6 +14,7 @@ namespace Twig\Tests\NodeVisitor; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Twig\Environment; +use Twig\Error\Error; use Twig\Extension\AbstractExtension; use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ConstantExpression; @@ -21,6 +22,7 @@ use Twig\Node\Node; use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\TwigFilter; class EscaperTest extends TestCase { @@ -46,6 +48,32 @@ class EscaperTest extends TestCase yield 'escaping in an embedded template only' => [false, '{% embed "embedded" %}{% block content %}{{ foo|e }}{% endblock %}{% endembed %}', 1, '<br>']; } + public function testCompilationErrorInsideAnAutoescapeTagDoesNotAffectTheNextTemplate(): void + { + $env = new Environment(new ArrayLoader([ + 'broken.html' => '{% autoescape false %}{{ foo|failing|nl2br }}{% endautoescape %}', + 'index.html' => '{{ foo }}', + 'index.txt' => '{{ foo }}', + ]), ['autoescape' => 'name']); + $env->addExtension(new class extends AbstractExtension { + public function getFilters(): array + { + return [new TwigFilter('failing', static fn ($value) => $value, ['is_safe_callback' => static function (): array { + throw new \LogicException('Unable to compute the safety of the filter.'); + }])]; + } + }); + + try { + $env->load('broken.html'); + $this->fail('Compiling the template should fail.'); + } catch (Error) { + } + + $this->assertSame('
', $env->render('index.txt', ['foo' => '
'])); + $this->assertSame('<br>', $env->render('index.html', ['foo' => '
'])); + } + public function testEscapeFilterAddedByALaterVisitorStillEscapes(): void { $env = new Environment(new ArrayLoader(['index' => '{{ "
" }}']), ['autoescape' => false]);