Reject cross-environment template wrappers in block chains

This commit is contained in:
Fabien Potencier
2026-09-12 09:57:01 +02:00
parent c1fc112047
commit 83e8f7e123
5 changed files with 18 additions and 19 deletions
+2 -7
View File
@@ -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);
}
-4
View File
@@ -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;
}
+9 -1
View File
@@ -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));
}
+3 -3
View File
@@ -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();
}
+4 -4
View File
@@ -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());