Merge branch '3.x' into 4.x

* 3.x:
  Improve the way one can deprecate a Twig callable
  Bump version
  Prepare the 3.14.0 release
  Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
  Fix test
  fix the version mergeGlobals() is deprecated since
  Tweak code
  Fix CS
  Add more tests
  Remove unused private methods
  Bump version
This commit is contained in:
Fabien Potencier
2024-09-09 21:19:33 +02:00
15 changed files with 319 additions and 70 deletions
+16 -10
View File
@@ -277,21 +277,27 @@ filter: ``('a', 'b', 'foo')``.
Deprecated Filters
~~~~~~~~~~~~~~~~~~
You can mark a filter as being deprecated by setting the ``deprecated`` option
to ``true``. You can also give an alternative filter that replaces the
deprecated one when that makes sense::
.. versionadded:: 3.15
The ``deprecation_info`` option was added in Twig 3.15.
You can mark a filter as being deprecated by setting the ``deprecation_info``
option::
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => true, 'alternative' => 'new_one']);
}, ['deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.11', 'new_one')]);
You can also set the ``deprecating_package`` option to specify the package that
is deprecating the filter, and ``deprecated`` can be set to the package version
when the filter was deprecated::
The ``DeprecatedCallableInfo`` constructor takes the following parameters:
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']);
* The Composer package name that defines the filter;
* The version when the filter was deprecated.
Optionally, you can also provide the following parameters about an alternative:
* The package name that contains the alternative filter;
* The alternative filter name that replaces the deprecated one;
* The package version that added the alternative filter.
When a filter is deprecated, Twig emits a deprecation notice when compiling a
template using it. See :ref:`deprecation-notices` for more information.
+46 -1
View File
@@ -39,10 +39,35 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
'needs_context' => false,
'needs_charset' => false,
'is_variadic' => false,
'deprecation_info' => null,
'deprecated' => false,
'deprecating_package' => '',
'alternative' => null,
], $options);
if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) {
throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class));
}
if ($this->options['deprecated']) {
if ($this->options['deprecation_info']) {
throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.');
}
trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.');
$this->options['deprecation_info'] = new DeprecatedCallableInfo(
$this->options['deprecating_package'],
$this->options['deprecated'],
null,
$this->options['alternative'],
);
}
if ($this->options['deprecation_info']) {
$this->options['deprecation_info']->setName($name);
$this->options['deprecation_info']->setType($this->getType());
}
}
public function __toString(): string
@@ -107,21 +132,41 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
public function isDeprecated(): bool
{
return (bool) $this->options['deprecated'];
return (bool) $this->options['deprecation_info'];
}
public function triggerDeprecation(?string $file = null, ?int $line = null): void
{
$this->options['deprecation_info']->triggerDeprecation($file, $line);
}
/**
* @deprecated since Twig 3.15
*/
public function getDeprecatingPackage(): string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return $this->options['deprecating_package'];
}
/**
* @deprecated since Twig 3.15
*/
public function getDeprecatedVersion(): string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
}
/**
* @deprecated since Twig 3.15
*/
public function getAlternative(): ?string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return $this->options['alternative'];
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DeprecatedCallableInfo
{
private string $type;
private string $name;
public function __construct(
private string $package,
private string $version,
private ?string $altName = null,
private ?string $altPackage = null,
private ?string $altVersion = null,
) {
}
public function setType(string $type): void
{
$this->type = $type;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function triggerDeprecation(?string $file = null, ?int $line = null): void
{
$message = \sprintf('Twig %s "%s" is deprecated', ucfirst($this->type), $this->name);
if ($this->altName) {
$message .= \sprintf('; use "%s"', $this->altName);
if ($this->altPackage) {
$message .= \sprintf(' from the "%s" package', $this->altPackage);
}
if ($this->altVersion) {
$message .= \sprintf(' (available since version %s)', $this->altVersion);
}
$message .= ' instead';
}
if ($file) {
$message .= \sprintf(' in %s', $file);
if ($line) {
$message .= \sprintf(' at line %d', $line);
}
}
$message .= '.';
trigger_deprecation($this->package, $this->version, $message);
}
}
+13
View File
@@ -815,6 +815,19 @@ class Environment
}
/**
<<<<<<< HEAD
=======
* @deprecated since Twig 3.14
*/
public function mergeGlobals(array $context): array
{
trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__);
return $context + $this->getGlobals();
}
/**
>>>>>>> 3.x
* @internal
*
* @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
+3 -22
View File
@@ -719,15 +719,8 @@ class ExpressionParser
if ($test->isDeprecated()) {
$stream = $this->parser->getStream();
$message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
if ($test->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $test->getAlternative());
}
$src = $stream->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
$test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
}
return $test;
@@ -743,14 +736,8 @@ class ExpressionParser
}
if ($function->isDeprecated()) {
$message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
if ($function->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $function->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
$function->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
}
return $function;
@@ -766,14 +753,8 @@ class ExpressionParser
}
if ($filter->isDeprecated()) {
$message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
if ($filter->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $filter->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
$filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
}
return $filter;
+8 -8
View File
@@ -11,6 +11,7 @@
namespace Twig\Extension;
use Twig\DeprecatedCallableInfo;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
@@ -1386,13 +1387,6 @@ final class CoreExtension extends AbstractExtension
if (!$alreadySandboxed = $sandbox->isSandboxed()) {
$sandbox->enableSandbox();
}
foreach ((\is_array($template) ? $template : [$template]) as $name) {
// if a Template instance is passed, it might have been instantiated outside of a sandbox, check security
if ($name instanceof TemplateWrapper || $name instanceof Template) {
$name->unwrap()->checkSecurity();
}
}
}
try {
@@ -1403,9 +1397,15 @@ final class CoreExtension extends AbstractExtension
if (!$ignoreMissing) {
throw $e;
}
return '';
}
return $loaded ? $loaded->render($variables) : '';
if ($isSandboxed) {
$loaded->unwrap()->checkSecurity();
}
return $loaded->render($variables);
} finally {
if ($isSandboxed && !$alreadySandboxed) {
$sandbox->disableSandbox();
-23
View File
@@ -406,14 +406,6 @@ final class ModuleNode extends Node
continue;
}
if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) {
continue;
}
if ($node instanceof BlockReferenceNode) {
continue;
}
$traitable = false;
break;
}
@@ -482,19 +474,4 @@ final class ModuleNode extends Node
throw new \LogicException('Trait templates can only be constant nodes.');
}
}
private function hasNodeOutputNodes(Node $node): bool
{
if ($node instanceof NodeOutputInterface) {
return true;
}
foreach ($node as $child) {
if ($this->hasNodeOutputNodes($child)) {
return true;
}
}
return false;
}
}
+1
View File
@@ -23,6 +23,7 @@ use Twig\TokenStream;
* {% types {foo: 'int', bar?: 'string'} %}
*
* @author Jeroen Versteeg <jeroen@alisqi.com>
*
* @internal
*/
final class TypesTokenParser extends AbstractTokenParser
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace Twig\Tests;
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use PHPUnit\Framework\TestCase;
use Twig\DeprecatedCallableInfo;
class DeprecatedCallableInfoTest extends TestCase
{
/**
* @dataProvider provideTestsForTriggerDeprecation
*/
public function testTriggerDeprecation($expected, DeprecatedCallableInfo $info)
{
$info->setType('function');
$info->setName('foo');
$deprecations = [];
try {
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
return false;
});
$info->triggerDeprecation('foo.twig', 1);
} finally {
restore_error_handler();
}
$this->assertSame([$expected], $deprecations);
}
public static function provideTestsForTriggerDeprecation(): iterable
{
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1')];
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" from the "all/bar" package (available since version 12.10) instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo', 'all/bar', '12.10')];
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" from the "all/bar" package instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo', 'all/bar')];
yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo')];
}
public function testTriggerDeprecationWithoutFileOrLine()
{
$info = new DeprecatedCallableInfo('foo/bar', '1.1');
$info->setType('function');
$info->setName('foo');
$deprecations = [];
try {
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
return false;
});
$info->triggerDeprecation();
$info->triggerDeprecation('foo.twig');
} finally {
restore_error_handler();
}
$this->assertSame([
'Since foo/bar 1.1: Twig Function "foo" is deprecated.',
'Since foo/bar 1.1: Twig Function "foo" is deprecated in foo.twig.',
], $deprecations);
}
}
+39
View File
@@ -13,8 +13,13 @@ namespace Twig\Tests\Extension;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\SandboxExtension;
use Twig\Loader\ArrayLoader;
use Twig\Sandbox\SecurityError;
use Twig\Sandbox\SecurityPolicy;
class CoreTest extends TestCase
{
@@ -337,6 +342,40 @@ class CoreTest extends TestCase
[1, 42, "\x00\x34\x32"],
];
}
public function testSandboxedInclude()
{
$twig = new Environment(new ArrayLoader([
'index' => '{{ include("included", sandboxed: true) }}',
'included' => '{{ "included"|e }}',
]));
$policy = new SecurityPolicy(allowedFunctions: ['include']);
$sandbox = new SandboxExtension($policy, false);
$twig->addExtension($sandbox);
// We expect a compile error
$this->expectException(SecurityError::class);
$twig->render('index');
}
public function testSandboxedIncludeWithPreloadedTemplate()
{
$twig = new Environment(new ArrayLoader([
'index' => '{{ include("included", sandboxed: true) }}',
'included' => '{{ "included"|e }}',
]));
$policy = new SecurityPolicy(allowedFunctions: ['include']);
$sandbox = new SandboxExtension($policy, false);
$twig->addExtension($sandbox);
// The template is loaded without the sandbox enabled
// so, no compile error
$twig->load('included');
// We expect a runtime error
$this->expectException(SecurityError::class);
$twig->render('index');
}
}
final class CoreTestIteratorAggregate implements \IteratorAggregate
+2 -2
View File
@@ -1,8 +1,8 @@
--TEST--
Functions can be deprecated_function
--DEPRECATION--
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 2.
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 4.
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated; use "not_deprecated_function" instead in index.twig at line 2.
Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated; use "not_deprecated_function" instead in index.twig at line 4.
--TEMPLATE--
{{ deprecated_function() }}
@@ -5,8 +5,8 @@
--TEMPLATE(foo.twig)--
{{ foo|e }}
{{ foo|e }}
{{ 'foo'|e }}
{{ 'foo'|e }}
--DATA--
return []
--EXCEPTION--
@@ -0,0 +1,38 @@
--TEST--
conditional "block" tag with "extends" tag (nested)
--TEMPLATE--
{% extends "layout.twig" %}
{% block content_base %}
{{ parent() -}}
index
{% endblock %}
{% block content_layout -%}
{{ parent() -}}
nested_index
{% endblock %}
--TEMPLATE(layout.twig)--
{% extends "base.twig" %}
{% block content_base %}
{{ parent() -}}
layout
{% if true -%}
{% block content_layout -%}
nested_layout
{% endblock -%}
{% endif %}
{% endblock %}
--TEMPLATE(base.twig)--
{% block content_base %}
base
{% endblock %}
--DATA--
return []
--EXPECT--
base
layout
nested_layout
nested_index
index
+2 -1
View File
@@ -11,6 +11,7 @@ namespace Twig\Tests;
* file that was distributed with this source code.
*/
use Twig\DeprecatedCallableInfo;
use Twig\Extension\AbstractExtension;
use Twig\Extension\DebugExtension;
use Twig\Extension\SandboxExtension;
@@ -183,7 +184,7 @@ class TwigTestExtension extends AbstractExtension
new TwigFunction('*_path', [$this, 'dynamic_path']),
new TwigFunction('*_foo_*_bar', [$this, 'dynamic_foo']),
new TwigFunction('anon_foo', function ($name) { return '*'.$name.'*'; }),
new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar', 'alternative' => 'not_deprecated_function']),
new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1', 'not_deprecated_function')]),
];
}
+2 -1
View File
@@ -12,6 +12,7 @@ namespace Twig\Tests\Util;
*/
use PHPUnit\Framework\TestCase;
use Twig\DeprecatedCallableInfo;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\TwigFunction;
@@ -22,7 +23,7 @@ class DeprecationCollectorTest extends TestCase
public function testCollect()
{
$twig = new Environment(new ArrayLoader());
$twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']));
$twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1')]));
$collector = new DeprecationCollector($twig);
$deprecations = $collector->collect(new Iterator());