Make the include() function return a Markup object

This commit is contained in:
Fabien Potencier
2026-06-02 23:19:08 +02:00
parent ecf0ebf78b
commit a3eda4b1fd
6 changed files with 66 additions and 3 deletions
+1
View File
@@ -4,6 +4,7 @@
* Stop reporting a skipped test in `IntegrationTestCase` when there is no legacy test to run
* Make the `IntegrationTestCase` and `NodeTestCase` test helpers compatible with PHPUnit 11
* Cast printed expressions to string so values that cannot be converted to a string (arrays, non-`Stringable` objects, ...) report a usable stack trace at the print location
* Make the `include()` function return a `Markup` object so an assigned result is not re-escaped when printed
* Skip the sandbox `__toString` check on arguments whose PHP parameter type cannot implicitly coerce to string
# 3.27.1 (2026-05-30)
+12
View File
@@ -8,6 +8,18 @@ The ``include`` function returns the rendered content of a template:
{{ include('template.html.twig') }}
{{ include(some_var) }}
The returned content is a ``\Twig\Markup`` instance, so it is considered safe
and is not escaped again when you store it in a variable and print it later:
.. code-block:: twig
{% set body = include('body.html.twig') %}
{{ body }} {# rendered as-is, not re-escaped #}
Beware that, like any safe value, it is not re-escaped for the context it ends
up in, so only embed it in the same context it was rendered for (typically
HTML).
Included templates have access to the variables of the active context.
If you are using the filesystem loader, the templates are looked for in the
+6 -2
View File
@@ -1490,9 +1490,11 @@ final class CoreExtension extends AbstractExtension
* @param bool $ignoreMissing Whether to ignore missing templates or not
* @param bool $sandboxed Whether to sandbox the template or not
*
* @return string|Markup
*
* @internal
*/
public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string
public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
{
$alreadySandboxed = false;
$sandbox = null;
@@ -1519,7 +1521,9 @@ final class CoreExtension extends AbstractExtension
return '';
}
return $loaded->render($variables);
$rendered = $loaded->render($variables);
return '' === $rendered ? '' : new Markup($rendered, $env->getCharset());
} finally {
if ($isSandboxed && !$alreadySandboxed) {
$sandbox->disableSandbox();
+36
View File
@@ -29,6 +29,7 @@ use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\SandboxExtension;
use Twig\Loader\ArrayLoader;
use Twig\Markup;
use Twig\Sandbox\SecurityError;
use Twig\Sandbox\SecurityPolicy;
@@ -417,6 +418,41 @@ class CoreTest extends TestCase
$twig->render('index');
}
public function testSandboxedIncludeResultStaysEscapedWhenAssigned()
{
$twig = new Environment(new ArrayLoader([
'index' => "{% set body = include('included', sandboxed: true) %}[{{ body }}]",
'included' => '{{ evil }}',
]), ['autoescape' => 'html']);
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], ['escape'], [], [], ['include']), false));
$this->assertSame('[&lt;script&gt;]', $twig->render('index', ['evil' => '<script>']));
}
public function testIncludeReturnsMarkupForRenderedContent()
{
$twig = new Environment(new ArrayLoader(['included' => 'content']));
$result = CoreExtension::include($twig, [], 'included');
$this->assertInstanceOf(Markup::class, $result);
$this->assertSame('content', (string) $result);
}
public function testIncludeReturnsAnEmptyStringForEmptyContent()
{
$twig = new Environment(new ArrayLoader(['included' => '']));
$this->assertSame('', CoreExtension::include($twig, [], 'included'));
}
public function testIncludeReturnsAnEmptyStringForIgnoredMissingTemplate()
{
$twig = new Environment(new ArrayLoader([]));
$this->assertSame('', CoreExtension::include($twig, [], 'missing', ignoreMissing: true));
}
public function testLastModified()
{
$this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified());
@@ -23,6 +23,6 @@ class StringLoaderExtensionTest extends TestCase
{
$twig = new Environment(new ArrayLoader());
$twig->addExtension(new StringLoaderExtension());
$this->assertSame('something', CoreExtension::include($twig, [], StringLoaderExtension::templateFromString($twig, 'something')));
$this->assertSame('something', (string) CoreExtension::include($twig, [], StringLoaderExtension::templateFromString($twig, 'something')));
}
}
@@ -0,0 +1,10 @@
--TEST--
"include" function returns Markup so an assigned result is not re-escaped
--TEMPLATE--
{% set assigned = include("foo.twig") %}[{{ assigned }}]
--TEMPLATE(foo.twig)--
{{- "a & b"|escape -}}
--DATA--
return []
--EXPECT--
[a &amp; b]