From 83e8f7e123e3f29c6bdf54af2b94e883339717d3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 12 Sep 2026 09:57:01 +0200 Subject: [PATCH] Reject cross-environment template wrappers in block chains --- src/BlockChain.php | 9 ++------- src/Template.php | 4 ---- src/TemplateWrapper.php | 10 +++++++++- tests/BlockChainTest.php | 6 +++--- tests/TemplateTest.php | 8 ++++---- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/BlockChain.php b/src/BlockChain.php index e68f145da..7b21c595f 100644 --- a/src/BlockChain.php +++ b/src/BlockChain.php @@ -49,12 +49,11 @@ final class BlockChain throw new \TypeError(\sprintf('Block chain templates must be strings or "%s" instances, "%s" given.', TemplateWrapper::class, get_debug_type($template))); } - $template = $template->unwrap(); if (!$template->isOwnedBy($env)) { throw new \LogicException('A block chain cannot contain templates from different Twig environments.'); } - $this->templates[] = $template; + $this->templates[] = $template->unwrap($env); } if (!$this->templates) { @@ -173,11 +172,7 @@ final class BlockChain $parent = $template->getParent($context); $fixed = $fixed && $template->hasFixedParent(); - // a dynamic parent expression can evaluate to a template from another environment - $template = $parent instanceof TemplateWrapper ? $parent->unwrap() : $parent; - if (false !== $template && !$template->isOwnedBy($this->env)) { - throw new \LogicException('A block chain cannot contain templates from different Twig environments.'); - } + $template = $parent; } while (false !== $template); } diff --git a/src/Template.php b/src/Template.php index 17842382a..c2c1d37a4 100644 --- a/src/Template.php +++ b/src/Template.php @@ -78,10 +78,6 @@ abstract class Template public function getParent(array $context): self|false { if (null !== $this->parent) { - if ($this->parent instanceof TemplateWrapper) { - $this->parent = $this->load($this->parent, -1); - } - return $this->parent; } diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index 185b565e1..ab1c414fd 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -96,12 +96,20 @@ final class TemplateWrapper return $this->template->getTemplateName(); } + /** + * @internal + */ + public function isOwnedBy(Environment $env): bool + { + return $this->env === $env && $this->template->isOwnedBy($env); + } + /** * @internal */ public function unwrap(Environment $env): Template { - if ($this->env !== $env) { + if (!$this->isOwnedBy($env)) { throw new RuntimeError(\sprintf('A "%s" can only be used with the "%s" that created it.', self::class, Environment::class)); } diff --git a/tests/BlockChainTest.php b/tests/BlockChainTest.php index f096575fa..85e230d65 100644 --- a/tests/BlockChainTest.php +++ b/tests/BlockChainTest.php @@ -253,7 +253,7 @@ class BlockChainTest extends TestCase { $twig = new Environment(new ArrayLoader(['theme' => ''])); $other = new Environment(new ArrayLoader(['theme' => ''])); - $wrapper = new TemplateWrapper($twig, $other->load('theme')->unwrap()); + $wrapper = new TemplateWrapper($twig, $other->load('theme')->unwrap($other)); $this->expectException(\LogicException::class); $this->expectExceptionMessage('A block chain cannot contain templates from different Twig environments.'); @@ -267,8 +267,8 @@ class BlockChainTest extends TestCase $other = new Environment(new ArrayLoader(['parent' => ''])); $chain = new BlockChain($twig, ['theme'], ['parent' => $other->load('parent')]); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('A block chain cannot contain templates from different Twig environments.'); + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('A "Twig\TemplateWrapper" can only be used with the "Twig\Environment" that created it in "theme" at line 1.'); $chain->getBlockNames(); } diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 86997d2db..8db4b2113 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -236,7 +236,7 @@ class TemplateTest extends TestCase 'parent' => '{% block content %}{{ missing.value }}{% endblock %}', 'child' => '{% extends "parent" %}', ]), ['debug' => $debug, 'strict_variables' => true, 'use_yield' => false]); - $template = $twig->load('child')->unwrap(); + $template = $twig->load('child')->unwrap($twig); $level = ob_get_level(); try { @@ -261,14 +261,14 @@ class TemplateTest extends TestCase 'dynamic_parent' => '{% extends parent %}', ])); - $this->assertTrue($twig->load('no_parent')->unwrap()->hasFixedParent()); + $this->assertTrue($twig->load('no_parent')->unwrap($twig)->hasFixedParent()); - $constant = $twig->load('constant_parent')->unwrap(); + $constant = $twig->load('constant_parent')->unwrap($twig); $this->assertFalse($constant->hasFixedParent()); $constant->getParent([]); $this->assertTrue($constant->hasFixedParent()); - $dynamic = $twig->load('dynamic_parent')->unwrap(); + $dynamic = $twig->load('dynamic_parent')->unwrap($twig); $this->assertFalse($dynamic->hasFixedParent()); $dynamic->getParent(['parent' => 'no_parent']); $this->assertFalse($dynamic->hasFixedParent());