From bf6b7ae53dbb5253f0b965cd0f9b54110150bd8a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 8 Sep 2024 17:26:18 +0200 Subject: [PATCH 01/11] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3bc9a51d5..8d16b9428 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.13.1 (2024-XX-XX) +# 3.14.0 (2024-XX-XX) * Add the possibility to reset globals via `Environment::resetGlobals()` * Deprecate `Environment::mergeGlobals()` diff --git a/src/Environment.php b/src/Environment.php index 41bb518eb..de241b643 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.13.1-DEV'; - public const VERSION_ID = 31301; + public const VERSION = '3.14.0-DEV'; + public const VERSION_ID = 31400; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 13; - public const RELEASE_VERSION = 1; + public const MINOR_VERSION = 14; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 28dc912c0e2b3dfec22ee0d92c334fee21599873 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 08:15:28 +0200 Subject: [PATCH 02/11] Remove unused private methods --- src/Node/ModuleNode.php | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index b57e643e8..d2fb216b1 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -418,14 +418,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; } @@ -494,19 +486,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; - } } From 45e167a514ff0d53878be6a7c145336ccfdc4f03 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 08:41:21 +0200 Subject: [PATCH 03/11] Add more tests --- .../inheritance/conditional_block_nested.test | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/Fixtures/tags/inheritance/conditional_block_nested.test diff --git a/tests/Fixtures/tags/inheritance/conditional_block_nested.test b/tests/Fixtures/tags/inheritance/conditional_block_nested.test new file mode 100644 index 000000000..1f99dfb61 --- /dev/null +++ b/tests/Fixtures/tags/inheritance/conditional_block_nested.test @@ -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 From 995e7c2270789c21ae3df5b5061101e139cd7026 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 08:49:09 +0200 Subject: [PATCH 04/11] Fix CS --- src/Test/IntegrationTestCase.php | 1 + src/TokenParser/TypesTokenParser.php | 1 + tests/LexerTest.php | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 78b0171e1..8690a809e 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -31,6 +31,7 @@ abstract class IntegrationTestCase extends TestCase { /** * @deprecated since Twig 3.13, use getFixturesDirectory() instead. + * * @return string */ protected function getFixturesDir() diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index 2172ed263..2e0850e7e 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -23,6 +23,7 @@ use Twig\TokenStream; * {% types {foo: 'int', bar?: 'string'} %} * * @author Jeroen Versteeg + * * @internal */ final class TypesTokenParser extends AbstractTokenParser diff --git a/tests/LexerTest.php b/tests/LexerTest.php index d8da55458..a9cb7cb62 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -231,6 +231,7 @@ class LexerTest extends TestCase /** * @group legacy + * * @dataProvider getStringWithEscapedDelimiterProducingDeprecation */ public function testStringWithEscapedDelimiterProducingDeprecation(string $template, string $expected, string $expectedDeprecation) From 064e07965465eb33f2b2ca47c188681039f6b876 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 10:51:41 +0200 Subject: [PATCH 05/11] Tweak code --- src/Extension/CoreExtension.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index ee0a1dbaf..fa97918b6 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1448,9 +1448,11 @@ final class CoreExtension extends AbstractExtension if (!$ignoreMissing) { throw $e; } + + return ''; } - return $loaded ? $loaded->render($variables) : ''; + return $loaded->render($variables); } finally { if ($isSandboxed && !$alreadySandboxed) { $sandbox->disableSandbox(); From f72c93dbd22310f729203881351c75c3d44b56ec Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 9 Sep 2024 10:58:08 +0200 Subject: [PATCH 06/11] fix the version mergeGlobals() is deprecated since --- doc/deprecated.rst | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 0490c2ded..a353518f0 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -227,7 +227,7 @@ Testing Utilities Environment ----------- -* The ``Twig\Environment::mergeGlobals()`` method is deprecated as of Twig 3.13 +* The ``Twig\Environment::mergeGlobals()`` method is deprecated as of Twig 3.14 and will be removed in Twig 4.0: Before:: diff --git a/src/Environment.php b/src/Environment.php index de241b643..fb5342b62 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -837,11 +837,11 @@ class Environment } /** - * @deprecated since Twig 3.13 + * @deprecated since Twig 3.14 */ public function mergeGlobals(array $context): array { - trigger_deprecation('twig/twig', '3.13', 'The "%s" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__); return $context + $this->getGlobals(); } From 795720230c8e90d91dfaed3bfd749160f7dc1e03 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 11:10:45 +0200 Subject: [PATCH 07/11] Fix test --- tests/Fixtures/functions/include/sandbox.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Fixtures/functions/include/sandbox.test b/tests/Fixtures/functions/include/sandbox.test index ebfdb1eb8..ec7e61e97 100644 --- a/tests/Fixtures/functions/include/sandbox.test +++ b/tests/Fixtures/functions/include/sandbox.test @@ -5,8 +5,8 @@ --TEMPLATE(foo.twig)-- -{{ foo|e }} -{{ foo|e }} +{{ 'foo'|e }} +{{ 'foo'|e }} --DATA-- return [] --EXCEPTION-- From 11f68e2aeb526bfaf638e30d4420d8a710f3f7c6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 10:51:06 +0200 Subject: [PATCH 08/11] Fix a security issue when an included sandboxed template has been loaded before without the sandbox context --- src/Extension/CoreExtension.php | 11 ++++------ tests/Extension/CoreTest.php | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fa97918b6..3ed27a35c 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1431,13 +1431,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 { @@ -1452,6 +1445,10 @@ final class CoreExtension extends AbstractExtension return ''; } + if ($isSandboxed) { + $loaded->unwrap()->checkSecurity(); + } + return $loaded->render($variables); } finally { if ($isSandboxed && !$alreadySandboxed) { diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index e869f96c7..6bb74807f 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -12,8 +12,13 @@ namespace Twig\Tests\Extension; */ 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 { @@ -354,6 +359,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 From 126b2c97818dbff0cdf3fbfc881aedb3d40aae72 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 19:55:12 +0200 Subject: [PATCH 09/11] Prepare the 3.14.0 release --- CHANGELOG | 3 ++- src/Environment.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8d16b9428..44c79b133 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ -# 3.14.0 (2024-XX-XX) +# 3.14.0 (2024-09-09) + * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context * Add the possibility to reset globals via `Environment::resetGlobals()` * Deprecate `Environment::mergeGlobals()` diff --git a/src/Environment.php b/src/Environment.php index fb5342b62..24e55e979 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.14.0-DEV'; + public const VERSION = '3.14.0'; public const VERSION_ID = 31400; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 14; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 247ee93df93f82104546169d35d1582b03defb06 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 21:04:19 +0200 Subject: [PATCH 10/11] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 44c79b133..d76769421 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.15.0 (2024-XX-XX) + + * n/a + # 3.14.0 (2024-09-09) * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context diff --git a/src/Environment.php b/src/Environment.php index 24e55e979..06a306b89 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.14.0'; - public const VERSION_ID = 31400; + public const VERSION = '3.15.0-DEV'; + public const VERSION_ID = 31500; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 14; + public const MINOR_VERSION = 15; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 34ae0311d94a97ae43282837ca2e12d7275b1b4a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 16:17:33 +0200 Subject: [PATCH 11/11] Improve the way one can deprecate a Twig callable --- CHANGELOG | 2 +- doc/advanced.rst | 48 ++++++++++---- doc/deprecated.rst | 19 ++++++ src/AbstractTwigCallable.php | 47 +++++++++++++- src/DeprecatedCallableInfo.php | 67 ++++++++++++++++++++ src/ExpressionParser.php | 25 +------- src/Extension/CoreExtension.php | 3 +- tests/DeprecatedCallableInfoTest.php | 80 ++++++++++++++++++++++++ tests/Fixtures/functions/deprecated.test | 4 +- tests/IntegrationTest.php | 3 +- tests/Util/DeprecationCollectorTest.php | 3 +- 11 files changed, 260 insertions(+), 41 deletions(-) create mode 100644 src/DeprecatedCallableInfo.php create mode 100644 tests/DeprecatedCallableInfoTest.php diff --git a/CHANGELOG b/CHANGELOG index d76769421..eefd094ac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.15.0 (2024-XX-XX) - * n/a + * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) # 3.14.0 (2024-09-09) diff --git a/doc/advanced.rst b/doc/advanced.rst index 6b3f1118e..01f03d980 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -277,29 +277,53 @@ 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')]); -.. versionadded:: 3.11 +The ``DeprecatedCallableInfo`` constructor takes the following parameters: - The ``deprecating_package`` option was added in Twig 3.11. +* The Composer package name that defines the filter; +* The version when the filter was deprecated. -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:: +Optionally, you can also provide the following parameters about an alternative: - $filter = new \Twig\TwigFilter('obsolete', function () { - // ... - }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']); +* 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. +.. note:: + + Before Twig 3.15, 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:: + + $filter = new \Twig\TwigFilter('obsolete', function () { + // ... + }, ['deprecated' => true, 'alternative' => 'new_one']); + + .. versionadded:: 3.11 + + The ``deprecating_package`` option was added in Twig 3.11. + + 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:: + + $filter = new \Twig\TwigFilter('obsolete', function () { + // ... + }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']); + Functions --------- diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a353518f0..c280702a5 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -237,3 +237,22 @@ Environment After:: $context += $twig->getGlobals(); + +Functions/Filters/Tests +----------------------- + +* The ``deprecated``, ``deprecating_package``, ``alternative`` options on Twig + functions/filters/Tests are deprecated as of Twig 3.15, and will be removed + in Twig 4.0. Use the ``deprecation_info`` option instead: + + Before:: + + $twig->addFunction(new TwigFunction('foo', 'foo', [ + 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig', + ])); + + After:: + + $twig->addFunction(new TwigFunction('foo', 'foo', [ + 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12'), + ])); diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index f67184300..2e9b34d18 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -33,10 +33,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 @@ -111,21 +136,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']; } diff --git a/src/DeprecatedCallableInfo.php b/src/DeprecatedCallableInfo.php new file mode 100644 index 000000000..2db9f3d28 --- /dev/null +++ b/src/DeprecatedCallableInfo.php @@ -0,0 +1,67 @@ + + */ +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); + } +} diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index dc4a6015d..a610a390e 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -773,15 +773,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; @@ -797,14 +790,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; @@ -820,14 +807,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; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 3ed27a35c..550dc0f38 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -11,6 +11,7 @@ namespace Twig\Extension; +use Twig\DeprecatedCallableInfo; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -217,7 +218,7 @@ final class CoreExtension extends AbstractExtension new TwigFilter('striptags', [self::class, 'striptags']), new TwigFilter('trim', [self::class, 'trim']), new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig']), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]), // array helpers new TwigFilter('join', [self::class, 'join']), diff --git a/tests/DeprecatedCallableInfoTest.php b/tests/DeprecatedCallableInfoTest.php new file mode 100644 index 000000000..4e6d1583c --- /dev/null +++ b/tests/DeprecatedCallableInfoTest.php @@ -0,0 +1,80 @@ +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); + } +} diff --git a/tests/Fixtures/functions/deprecated.test b/tests/Fixtures/functions/deprecated.test index 355e43303..42f2a5dd6 100644 --- a/tests/Fixtures/functions/deprecated.test +++ b/tests/Fixtures/functions/deprecated.test @@ -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() }} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 65f207d90..49ccf76cb 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -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; @@ -185,7 +186,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')]), ]; } diff --git a/tests/Util/DeprecationCollectorTest.php b/tests/Util/DeprecationCollectorTest.php index a3010d14d..635a67f40 100644 --- a/tests/Util/DeprecationCollectorTest.php +++ b/tests/Util/DeprecationCollectorTest.php @@ -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; @@ -25,7 +26,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());