diff --git a/src/BlockChain.php b/src/BlockChain.php index 64fa8652d..d9320ed01 100644 --- a/src/BlockChain.php +++ b/src/BlockChain.php @@ -58,12 +58,11 @@ final class BlockChain throw new \TypeError(\sprintf('Block chain templates must be strings, "%s" or "%s" instances, "%s" given.', TemplateWrapper::class, self::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) { @@ -210,11 +209,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/Environment.php b/src/Environment.php index 2e0064353..0931c0da1 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -335,6 +335,8 @@ class Environment public function load(string|TemplateWrapper $name): TemplateWrapper { if ($name instanceof TemplateWrapper) { + $name->unwrap($this); + return $name; } @@ -471,7 +473,7 @@ class Environment $count = \count($names); foreach ($names as $name) { if ($name instanceof TemplateWrapper) { - return $name; + return $this->load($name); } if (1 !== $count && !$this->getLoader()->exists($name)) { diff --git a/src/Template.php b/src/Template.php index 24f78e96c..a25b05b41 100644 --- a/src/Template.php +++ b/src/Template.php @@ -80,9 +80,9 @@ abstract class Template * This method is for internal use only and should never be called * directly. * - * @return self|TemplateWrapper|false The parent template or false if there is no parent + * @return self|false The parent template or false if there is no parent */ - public function getParent(array $context): self|TemplateWrapper|false + public function getParent(array $context): self|false { if (null !== $this->parent) { return $this->parent; @@ -106,7 +106,10 @@ abstract class Template return false; } - if ($parent instanceof self || $parent instanceof TemplateWrapper) { + if ($parent instanceof TemplateWrapper) { + $parent = $this->load($parent, -1); + } + if ($parent instanceof self) { return $this->parents[$parent->getSourceContext()->getName()] = $parent; } @@ -265,11 +268,11 @@ abstract class Template { try { if (\is_array($template)) { - return $this->env->resolveTemplate($template)->unwrap(); + return $this->env->resolveTemplate($template)->unwrap($this->env); } if ($template instanceof TemplateWrapper) { - return $template->unwrap(); + return $template->unwrap($this->env); } if ($template === $this->getTemplateName()) { diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index c08fca5e3..ab1c414fd 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -11,6 +11,8 @@ namespace Twig; +use Twig\Error\RuntimeError; + /** * Exposes a template to userland. * @@ -96,11 +98,21 @@ final class TemplateWrapper /** * @internal - * - * @return Template */ - public function unwrap() + public function isOwnedBy(Environment $env): bool { + return $this->env === $env && $this->template->isOwnedBy($env); + } + + /** + * @internal + */ + public function unwrap(Environment $env): Template + { + if (!$this->isOwnedBy($env)) { + throw new RuntimeError(\sprintf('A "%s" can only be used with the "%s" that created it.', self::class, Environment::class)); + } + return $this->template; } } diff --git a/tests/BlockChainTest.php b/tests/BlockChainTest.php index f6258c424..59c076571 100644 --- a/tests/BlockChainTest.php +++ b/tests/BlockChainTest.php @@ -308,7 +308,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.'); @@ -322,8 +322,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/CallMacroTest.php b/tests/CallMacroTest.php index ec9b94b98..b1886f7c9 100644 --- a/tests/CallMacroTest.php +++ b/tests/CallMacroTest.php @@ -179,6 +179,6 @@ class CallMacroTest extends TestCase { $twig = new Environment(new ArrayLoader($templates)); - return $twig->load('index')->unwrap(); + return $twig->load('index')->unwrap($twig); } } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index e44487df6..6ace44a51 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -81,6 +81,17 @@ class EnvironmentTest extends TestCase $this->assertEquals('foo\u003Cbr\/\u0020\u003E foo\u003Cbr\/\u0020\u003E', $twig->render('js', ['bar' => 'foo
'])); } + public function testRejectsTemplateWrapperFromAnotherEnvironment(): void + { + $foreign = new Environment(new ArrayLoader(['index' => 'foreign'])); + $twig = new Environment(new ArrayLoader()); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('can only be used with the "Twig\\Environment" that created it'); + + $twig->load($foreign->load('index')); + } + public function escapingStrategyCallback($name) { return $name; diff --git a/tests/Sandbox/SandboxTest.php b/tests/Sandbox/SandboxTest.php index 5f8f88fdc..7c2ec3954 100644 --- a/tests/Sandbox/SandboxTest.php +++ b/tests/Sandbox/SandboxTest.php @@ -11,6 +11,7 @@ namespace Twig\Tests\Sandbox; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; @@ -221,6 +222,34 @@ class SandboxTest extends TestCase $sandbox->render('index'); } + /** + * @dataProvider provideForeignTemplateWrapperUsages + */ + #[DataProvider('provideForeignTemplateWrapperUsages')] + public function testRejectsTemplateWrapperFromAnotherEnvironment(string $template, string $foreignTemplate, array $tags = [], array $functions = []): void + { + $foreign = self::env(['foreign' => $foreignTemplate]); + $sandbox = new Sandbox(self::env(['index' => $template]), self::strictPolicy(tags: $tags, functions: $functions)); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('can only be used with the "Twig\\Environment" that created it'); + + $sandbox->render('index', ['foreign' => $foreign->load('foreign')]); + } + + public static function provideForeignTemplateWrapperUsages(): iterable + { + yield 'include tag' => ['{% include foreign %}', 'foreign content', ['include']]; + yield 'include function' => ['{{ include(foreign) }}', 'foreign content', [], ['include']]; + yield 'include function fallback' => ['{{ include(["missing", foreign]) }}', 'foreign content', [], ['include']]; + yield 'include_only function' => ['{{ include_only(foreign) }}', 'foreign content', [], ['include_only']]; + yield 'extends tag' => ['{% extends foreign %}', 'foreign content', ['extends']]; + yield 'embed tag' => ['{% embed foreign %}{% endembed %}', 'foreign content', ['embed', 'extends']]; + yield 'import tag' => ['{% import foreign as macros %}{{ macros.foo() }}', '{% macro foo() %}foreign content{% endmacro %}', ['import']]; + yield 'from tag' => ['{% from foreign import foo %}{{ foo() }}', '{% macro foo() %}foreign content{% endmacro %}', ['from']]; + yield 'block function' => ['{{ block("content", foreign) }}', '{% block content %}foreign content{% endblock %}', [], ['block']]; + } + public function testTheExtendsTagMustBeAllowed(): void { $templates = [ diff --git a/tests/TemplateWrapperTest.php b/tests/TemplateWrapperTest.php index 970020343..36185b877 100644 --- a/tests/TemplateWrapperTest.php +++ b/tests/TemplateWrapperTest.php @@ -26,10 +26,23 @@ use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Loader\ArrayLoader; +use Twig\Template; use Twig\TwigFunction; class TemplateWrapperTest extends TestCase { + public function testUnwrapChecksTheEnvironment(): void + { + $twig = new Environment(new ArrayLoader(['index' => 'content'])); + $wrapper = $twig->load('index'); + + $this->assertInstanceOf(Template::class, $wrapper->unwrap($twig)); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('can only be used with the "Twig\\Environment" that created it'); + $wrapper->unwrap(new Environment(new ArrayLoader())); + } + public function testHasGetBlocks(): void { $twig = new Environment(new ArrayLoader([