From 16abb69d12fa9ff5389b484db8134a9f3ddd7fa3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 8 Oct 2023 09:41:25 +0200 Subject: [PATCH 01/10] Deprecate internal extension functions in favor of methods on the extension classes --- CHANGELOG | 4 +++- composer.json | 1 + doc/deprecated.rst | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 14ba7576b17..17caf366e03 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ -# 3.8.1 (2023-XX-XX) +# 3.9.0 (2023-XX-XX) * Add return type for Symfony 7 compatibility * Fix premature loop exit in Security Policy lookup of allowed methods/properties + * Deprecate all internal extension functions in favor of methods on the extension classes + * Mark all extension functions as @internal # 3.8.0 (2023-11-21) diff --git a/composer.json b/composer.json index 1b1726fe882..31a248c2bbf 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "require": { "php": ">=7.2.5", "symfony/polyfill-php80": "^1.22", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-ctype": "^1.8" }, diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a5771ef4ec1..948dfb3ef43 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -10,3 +10,10 @@ Functions * The `twig_test_iterable` function is deprecated; use the native `is_iterable` instead. + +Extensions +---------- + +* All functions defined in Twig extensions are marked as internal as of Twig + 3.9.0, and will be removed in Twig 4.0. They have been replaced by internal + methods on their respective extension classes. From 6a18cda5aaad6fff27ebe32c6f753358837a4faa Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 18:04:05 +0100 Subject: [PATCH 02/10] Move functions for HtmlExtension --- extra/html-extra/HtmlExtension.php | 54 ++++++++++--------- extra/html-extra/Resources/functions.php | 23 ++++++++ .../html-extra/Tests/LegacyFunctionsTest.php | 26 +++++++++ extra/html-extra/composer.json | 2 + 4 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 extra/html-extra/Resources/functions.php create mode 100644 extra/html-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index ed740b47187..d5842bf500c 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -9,8 +9,10 @@ * file that was distributed with this source code. */ -namespace Twig\Extra\Html { +namespace Twig\Extra\Html; + use Symfony\Component\Mime\MimeTypes; +use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -34,7 +36,7 @@ public function getFilters(): array public function getFunctions(): array { return [ - new TwigFunction('html_classes', 'twig_html_classes'), + new TwigFunction('html_classes', [self::class, 'htmlClasses']), ]; } @@ -45,6 +47,8 @@ public function getFunctions(): array * be done before calling this filter. * * @return string The generated data URI + * + * @internal */ public function dataUri(string $data, string $mime = null, array $parameters = []): string { @@ -79,33 +83,31 @@ public function dataUri(string $data, string $mime = null, array $parameters = [ return $repr; } -} -} - -namespace { -use Twig\Error\RuntimeError; -function twig_html_classes(...$args): string -{ - $classes = []; - foreach ($args as $i => $arg) { - if (\is_string($arg)) { - $classes[] = $arg; - } elseif (\is_array($arg)) { - foreach ($arg as $class => $condition) { - if (!\is_string($class)) { - throw new RuntimeError(sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, \gettype($class))); - } - if (!$condition) { - continue; + /** + * @internal + */ + public static function htmlClasses(...$args): string + { + $classes = []; + foreach ($args as $i => $arg) { + if (\is_string($arg)) { + $classes[] = $arg; + } elseif (\is_array($arg)) { + foreach ($arg as $class => $condition) { + if (!\is_string($class)) { + throw new RuntimeError(sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, \gettype($class))); + } + if (!$condition) { + continue; + } + $classes[] = $class; } - $classes[] = $class; + } else { + throw new RuntimeError(sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, \gettype($arg))); } - } else { - throw new RuntimeError(sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, \gettype($arg))); } - } - return implode(' ', array_unique($classes)); -} + return implode(' ', array_unique($classes)); + } } diff --git a/extra/html-extra/Resources/functions.php b/extra/html-extra/Resources/functions.php new file mode 100644 index 00000000000..4458f3bf8b7 --- /dev/null +++ b/extra/html-extra/Resources/functions.php @@ -0,0 +1,23 @@ +assertSame(HtmlExtension::htmlClasses(['charset' => 'utf-8']), \twig_html_classes(['charset' => 'utf-8'])); + } +} diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index e67e53e5a82..ca7b2f62cf4 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/mime": "^5.4|^6.0|^7.0", "twig/twig": "^3.0" }, @@ -23,6 +24,7 @@ "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\Html\\" : "" }, "exclude-from-classmap": [ "/Tests/" From bff189f19adcca9329084ae7fb482456170ed785 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 17:59:49 +0100 Subject: [PATCH 03/10] Move functions for CssInlinerExtension --- .../cssinliner-extra/CssInlinerExtension.php | 21 ++++++++------- .../cssinliner-extra/Resources/functions.php | 23 ++++++++++++++++ .../Tests/LegacyFunctionsTest.php | 27 +++++++++++++++++++ extra/cssinliner-extra/composer.json | 2 ++ 4 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 extra/cssinliner-extra/Resources/functions.php create mode 100644 extra/cssinliner-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/cssinliner-extra/CssInlinerExtension.php b/extra/cssinliner-extra/CssInlinerExtension.php index 4d8b75a5623..679e9165716 100644 --- a/extra/cssinliner-extra/CssInlinerExtension.php +++ b/extra/cssinliner-extra/CssInlinerExtension.php @@ -20,17 +20,20 @@ class CssInlinerExtension extends AbstractExtension public function getFilters() { return [ - new TwigFilter('inline_css', 'Twig\\Extra\\CssInliner\\twig_inline_css', ['is_safe' => ['all']]), + new TwigFilter('inline_css', [self::class, 'inlineCss'], ['is_safe' => ['all']]), ]; } -} -function twig_inline_css(string $body, string ...$css): string -{ - static $inliner; - if (null === $inliner) { - $inliner = new CssToInlineStyles(); + /** + * @internal + */ + public static function inlineCss(string $body, string ...$css): string + { + static $inliner; + if (null === $inliner) { + $inliner = new CssToInlineStyles(); + } + + return $inliner->convert($body, implode("\n", $css)); } - - return $inliner->convert($body, implode("\n", $css)); } diff --git a/extra/cssinliner-extra/Resources/functions.php b/extra/cssinliner-extra/Resources/functions.php new file mode 100644 index 00000000000..c4b32db2d95 --- /dev/null +++ b/extra/cssinliner-extra/Resources/functions.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Twig\Extra\CssInliner; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_inline_css(string $body, string ...$css): string +{ + trigger_deprecation('twig/cssinliner-extra', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CssInlinerExtension::inlineCss($body, ...$css); +} diff --git a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php new file mode 100644 index 00000000000..20feef172c9 --- /dev/null +++ b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php @@ -0,0 +1,27 @@ +assertSame(CssInlinerExtension::inlineCss('

body

', 'p { color: red }'), twig_inline_css('

body

', 'p { color: red }')); + } +} diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index 32be7b44de4..0a279c61323 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "tijsverkoyen/css-to-inline-styles": "^2.0", "twig/twig": "^3.0" }, @@ -23,6 +24,7 @@ "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\CssInliner\\" : "" }, "exclude-from-classmap": [ "/Tests/" From e07e9b5be3cbcf924b3553d9e5976e26a6f39389 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 18:06:59 +0100 Subject: [PATCH 04/10] Move functions for InkyExtension --- extra/inky-extra/InkyExtension.php | 13 +++++---- extra/inky-extra/Resources/functions.php | 23 ++++++++++++++++ .../inky-extra/Tests/LegacyFunctionsTest.php | 27 +++++++++++++++++++ extra/inky-extra/composer.json | 2 ++ 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 extra/inky-extra/Resources/functions.php create mode 100644 extra/inky-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/inky-extra/InkyExtension.php b/extra/inky-extra/InkyExtension.php index 1ee2b515660..374cb7efbc5 100644 --- a/extra/inky-extra/InkyExtension.php +++ b/extra/inky-extra/InkyExtension.php @@ -20,12 +20,15 @@ class InkyExtension extends AbstractExtension public function getFilters() { return [ - new TwigFilter('inky_to_html', 'Twig\\Extra\\Inky\\twig_inky', ['is_safe' => ['html']]), + new TwigFilter('inky_to_html', [self::class, 'inky'], ['is_safe' => ['html']]), ]; } -} -function twig_inky(string $body): string -{ - return false === ($html = Pinky\transformString($body)->saveHTML()) ? '' : $html; + /** + * @internal + */ + public static function inky(string $body): string + { + return false === ($html = Pinky\transformString($body)->saveHTML()) ? '' : $html; + } } diff --git a/extra/inky-extra/Resources/functions.php b/extra/inky-extra/Resources/functions.php new file mode 100644 index 00000000000..feef69eabe8 --- /dev/null +++ b/extra/inky-extra/Resources/functions.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Twig\Extra\Inky; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_inky(string $body): string +{ + trigger_deprecation('twig/inky-extra', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return InkyExtension::inky($body); +} diff --git a/extra/inky-extra/Tests/LegacyFunctionsTest.php b/extra/inky-extra/Tests/LegacyFunctionsTest.php new file mode 100644 index 00000000000..26ed1e76fe1 --- /dev/null +++ b/extra/inky-extra/Tests/LegacyFunctionsTest.php @@ -0,0 +1,27 @@ +assertSame(InkyExtension::inky('

Foo

'), twig_inky('

Foo

')); + } +} diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index 2cb5a5ad308..9eacfc55b35 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "lorenzo/pinky": "^1.0.5", "twig/twig": "^3.0" }, @@ -23,6 +24,7 @@ "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\Inky\\" : "" }, "exclude-from-classmap": [ "/Tests/" From 69a89e0e77b101ac414022833d12a509f137304a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 18:24:26 +0100 Subject: [PATCH 05/10] Move functions for MarkdownExtension --- extra/markdown-extra/MarkdownExtension.php | 43 ++++++++++--------- extra/markdown-extra/Resources/functions.php | 23 ++++++++++ .../Tests/LegacyFunctionsTest.php | 27 ++++++++++++ extra/markdown-extra/composer.json | 2 + 4 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 extra/markdown-extra/Resources/functions.php create mode 100644 extra/markdown-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/markdown-extra/MarkdownExtension.php b/extra/markdown-extra/MarkdownExtension.php index 6c4296bbf1b..1647b66ff70 100644 --- a/extra/markdown-extra/MarkdownExtension.php +++ b/extra/markdown-extra/MarkdownExtension.php @@ -21,28 +21,31 @@ public function getFilters() { return [ new TwigFilter('markdown_to_html', ['Twig\\Extra\\Markdown\\MarkdownRuntime', 'convert'], ['is_safe' => ['all']]), - new TwigFilter('html_to_markdown', 'Twig\\Extra\\Markdown\\twig_html_to_markdown', ['is_safe' => ['all']]), + new TwigFilter('html_to_markdown', [self::class, 'htmlToMarkdown'], ['is_safe' => ['all']]), ]; } -} - -function twig_html_to_markdown(string $body, array $options = []): string -{ - static $converters; - - if (!class_exists(HtmlConverter::class)) { - throw new \LogicException('You cannot use the "html_to_markdown" filter as league/html-to-markdown is not installed; try running "composer require league/html-to-markdown".'); - } - $options += [ - 'hard_break' => true, - 'strip_tags' => true, - 'remove_nodes' => 'head style', - ]; - - if (!isset($converters[$key = serialize($options)])) { - $converters[$key] = new HtmlConverter($options); + /** + * @internal + */ + public static function htmlToMarkdown(string $body, array $options = []): string + { + static $converters; + + if (!class_exists(HtmlConverter::class)) { + throw new \LogicException('You cannot use the "html_to_markdown" filter as league/html-to-markdown is not installed; try running "composer require league/html-to-markdown".'); + } + + $options += [ + 'hard_break' => true, + 'strip_tags' => true, + 'remove_nodes' => 'head style', + ]; + + if (!isset($converters[$key = serialize($options)])) { + $converters[$key] = new HtmlConverter($options); + } + + return $converters[$key]->convert($body); } - - return $converters[$key]->convert($body); } diff --git a/extra/markdown-extra/Resources/functions.php b/extra/markdown-extra/Resources/functions.php new file mode 100644 index 00000000000..ad8da5a53b5 --- /dev/null +++ b/extra/markdown-extra/Resources/functions.php @@ -0,0 +1,23 @@ +assertSame(MarkdownExtension::htmlToMarkdown('

foo

'), html_to_markdown('

foo

')); + } +} diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 52dc07d9ec2..745ee502209 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "twig/twig": "^3.0" }, "require-dev": { @@ -26,6 +27,7 @@ "michelf/php-markdown": "^1.8|^2.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\Markdown\\" : "" }, "exclude-from-classmap": [ "/Tests/" From d4d016034c6599b1727e97fee50e0a614aaba723 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 11:29:00 +0100 Subject: [PATCH 06/10] Move functions for DebugExtension --- composer.json | 3 + src/Extension/DebugExtension.php | 58 ++++++++++---------- src/Resources/debug.php | 24 ++++++++ tests/Extension/LegacyDebugFunctionsTest.php | 30 ++++++++++ 4 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 src/Resources/debug.php create mode 100644 tests/Extension/LegacyDebugFunctionsTest.php diff --git a/composer.json b/composer.json index 31a248c2bbf..2802a450de7 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,9 @@ "psr/container": "^1.0|^2.0" }, "autoload": { + "files": [ + "src/Resources/debug.php" + ], "psr-4" : { "Twig\\" : "src/" } diff --git a/src/Extension/DebugExtension.php b/src/Extension/DebugExtension.php index c0f10d5a303..e023c7087b3 100644 --- a/src/Extension/DebugExtension.php +++ b/src/Extension/DebugExtension.php @@ -9,8 +9,12 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; use Twig\TwigFunction; +use Twig\Template; +use Twig\TemplateWrapper; final class DebugExtension extends AbstractExtension { @@ -27,38 +31,34 @@ public function getFunctions(): array ; return [ - new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), + new TwigFunction('dump', [self::class, 'dump'], ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), ]; } -} -} - -namespace { -use Twig\Environment; -use Twig\Template; -use Twig\TemplateWrapper; -function twig_var_dump(Environment $env, $context, ...$vars) -{ - if (!$env->isDebug()) { - return; - } - - ob_start(); - - if (!$vars) { - $vars = []; - foreach ($context as $key => $value) { - if (!$value instanceof Template && !$value instanceof TemplateWrapper) { - $vars[$key] = $value; + /** + * @internal + */ + public static function dump(Environment $env, $context, ...$vars) + { + if (!$env->isDebug()) { + return; + } + + ob_start(); + + if (!$vars) { + $vars = []; + foreach ($context as $key => $value) { + if (!$value instanceof Template && !$value instanceof TemplateWrapper) { + $vars[$key] = $value; + } } + + var_dump($vars); + } else { + var_dump(...$vars); } - - var_dump($vars); - } else { - var_dump(...$vars); + + return ob_get_clean(); } - - return ob_get_clean(); -} } diff --git a/src/Resources/debug.php b/src/Resources/debug.php new file mode 100644 index 00000000000..0041b4b79ac --- /dev/null +++ b/src/Resources/debug.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\DebugExtension; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_var_dump(Environment $env, $context, ...$vars) +{ + trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + DebugExtension::dump($env, $context, ...$vars); +} diff --git a/tests/Extension/LegacyDebugFunctionsTest.php b/tests/Extension/LegacyDebugFunctionsTest.php new file mode 100644 index 00000000000..4cab762006b --- /dev/null +++ b/tests/Extension/LegacyDebugFunctionsTest.php @@ -0,0 +1,30 @@ +assertSame(DebugExtension::dump($env, 'Foo'), twig_var_dump($env, 'Foo')); + } +} From 196e91dd722867dab329a427ec3f60eb59df8eb5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 11:38:36 +0100 Subject: [PATCH 07/10] Move functions for StringLoaderExtension --- composer.json | 3 +- src/Extension/StringLoaderExtension.php | 39 +++++++++---------- src/Resources/string_loader.php | 25 ++++++++++++ .../LegacyStringLoaderFunctionsTest.php | 30 ++++++++++++++ tests/Extension/StringLoaderExtensionTest.php | 2 +- 5 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 src/Resources/string_loader.php create mode 100644 tests/Extension/LegacyStringLoaderFunctionsTest.php diff --git a/composer.json b/composer.json index 2802a450de7..f337562389c 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ }, "autoload": { "files": [ - "src/Resources/debug.php" + "src/Resources/debug.php", + "src/Resources/string_loader.php" ], "psr-4" : { "Twig\\" : "src/" diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php index 7b451471007..fde395dd810 100644 --- a/src/Extension/StringLoaderExtension.php +++ b/src/Extension/StringLoaderExtension.php @@ -9,7 +9,10 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\TemplateWrapper; use Twig\TwigFunction; final class StringLoaderExtension extends AbstractExtension @@ -17,26 +20,22 @@ final class StringLoaderExtension extends AbstractExtension public function getFunctions(): array { return [ - new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]), + new TwigFunction('template_from_string', [self::class, 'templateFromString'], ['needs_environment' => true]), ]; } -} -} - -namespace { -use Twig\Environment; -use Twig\TemplateWrapper; -/** - * Loads a template from a string. - * - * {{ include(template_from_string("Hello {{ name }}")) }} - * - * @param string $template A template as a string or object implementing __toString() - * @param string $name An optional name of the template to be used in error messages - */ -function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper -{ - return $env->createTemplate((string) $template, $name); -} + /** + * Loads a template from a string. + * + * {{ include(template_from_string("Hello {{ name }}")) }} + * + * @param string $template A template as a string or object implementing __toString() + * @param string $name An optional name of the template to be used in error messages + * + * @internal + */ + public static function templateFromString(Environment $env, $template, string $name = null): TemplateWrapper + { + return $env->createTemplate((string) $template, $name); + } } diff --git a/src/Resources/string_loader.php b/src/Resources/string_loader.php new file mode 100644 index 00000000000..b074495ca5b --- /dev/null +++ b/src/Resources/string_loader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\StringLoaderExtension; +use Twig\TemplateWrapper; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper +{ + trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return StringLoaderExtension::templateFromString($env, $template, $name); +} diff --git a/tests/Extension/LegacyStringLoaderFunctionsTest.php b/tests/Extension/LegacyStringLoaderFunctionsTest.php new file mode 100644 index 00000000000..a6cb31df0c4 --- /dev/null +++ b/tests/Extension/LegacyStringLoaderFunctionsTest.php @@ -0,0 +1,30 @@ +assertSame(StringLoaderExtension::templateFromString($env, 'Foo')->render(), twig_template_from_string($env, 'Foo')->render()); + } +} diff --git a/tests/Extension/StringLoaderExtensionTest.php b/tests/Extension/StringLoaderExtensionTest.php index 363a0825ecb..e4fa2e3ba8c 100644 --- a/tests/Extension/StringLoaderExtensionTest.php +++ b/tests/Extension/StringLoaderExtensionTest.php @@ -21,6 +21,6 @@ public function testIncludeWithTemplateStringAndNoSandbox() { $twig = new Environment($this->createMock('\Twig\Loader\LoaderInterface')); $twig->addExtension(new StringLoaderExtension()); - $this->assertSame('something', twig_include($twig, [], twig_template_from_string($twig, 'something'))); + $this->assertSame('something', twig_include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); } } From 72071e8ece526c55c88142b6ace83f767a7908a0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 11:49:57 +0100 Subject: [PATCH 08/10] Move functions for EscaperExtension --- composer.json | 1 + src/Extension/EscaperExtension.php | 467 +++++++++++++++-------------- src/Resources/escaper.php | 35 +++ tests/Extension/EscaperTest.php | 48 +-- tests/IntegrationTest.php | 3 +- 5 files changed, 296 insertions(+), 258 deletions(-) create mode 100644 src/Resources/escaper.php diff --git a/composer.json b/composer.json index f337562389c..bceeedd328d 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "autoload": { "files": [ "src/Resources/debug.php", + "src/Resources/escaper.php", "src/Resources/string_loader.php" ], "psr-4" : { diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index ef8879dbdc6..88faa60a090 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -9,12 +9,19 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Error\RuntimeError; use Twig\FileExtensionEscapingStrategy; +use Twig\Markup; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Node; use Twig\NodeVisitor\EscaperNodeVisitor; use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TwigFilter; + final class EscaperExtension extends AbstractExtension { private $defaultStrategy; @@ -49,9 +56,9 @@ public function getNodeVisitors(): array public function getFilters(): array { return [ - new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]), + new TwigFilter('escape', [self::class, 'escape'], ['needs_environment' => true, 'is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('e', [self::class, 'escape'], ['needs_environment' => true, 'is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('raw', [self::class, 'raw'], ['is_safe' => ['all']]), ]; } @@ -132,285 +139,279 @@ public function addSafeClass(string $class, array $strategies) $this->safeLookup[$strategy][$class] = true; } } -} -} -namespace { -use Twig\Environment; -use Twig\Error\RuntimeError; -use Twig\Extension\EscaperExtension; -use Twig\Markup; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Node; + /** + * Marks a variable as being safe. + * + * @param string $string A PHP variable + * + * @internal + */ + public static function raw($string) + { + return $string; + } -/** - * Marks a variable as being safe. - * - * @param string $string A PHP variable - */ -function twig_raw_filter($string) -{ - return $string; -} + /** + * @internal + */ + public static function escapeFilterIsSafe(Node $filterArgs) + { + foreach ($filterArgs as $arg) { + if ($arg instanceof ConstantExpression) { + return [$arg->getAttribute('value')]; + } -/** - * Escapes a string. - * - * @param mixed $string The value to be escaped - * @param string $strategy The escaping strategy - * @param string $charset The charset - * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) - * - * @return string - */ -function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) -{ - if ($autoescape && $string instanceof Markup) { - return $string; + return []; + } + + return ['html']; } - if (!\is_string($string)) { - if (\is_object($string) && method_exists($string, '__toString')) { - if ($autoescape) { - $c = \get_class($string); - $ext = $env->getExtension(EscaperExtension::class); - if (!isset($ext->safeClasses[$c])) { - $ext->safeClasses[$c] = []; - foreach (class_parents($string) + class_implements($string) as $class) { - if (isset($ext->safeClasses[$class])) { - $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); - foreach ($ext->safeClasses[$class] as $s) { - $ext->safeLookup[$s][$c] = true; + /** + * Escapes a string. + * + * @param mixed $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @return string + * + * @internal + */ + public static function escape(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) + { + if ($autoescape && $string instanceof Markup) { + return $string; + } + + if (!\is_string($string)) { + if (\is_object($string) && method_exists($string, '__toString')) { + if ($autoescape) { + $c = \get_class($string); + $ext = $env->getExtension(EscaperExtension::class); + if (!isset($ext->safeClasses[$c])) { + $ext->safeClasses[$c] = []; + foreach (class_parents($string) + class_implements($string) as $class) { + if (isset($ext->safeClasses[$class])) { + $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); + foreach ($ext->safeClasses[$class] as $s) { + $ext->safeLookup[$s][$c] = true; + } } } } + if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { + return (string) $string; + } } - if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { - return (string) $string; - } - } - $string = (string) $string; - } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { - return $string; + $string = (string) $string; + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { + return $string; + } } - } - - if ('' === $string) { - return ''; - } - if (null === $charset) { - $charset = $env->getCharset(); - } + if ('' === $string) { + return ''; + } - switch ($strategy) { - case 'html': - // see https://www.php.net/htmlspecialchars - - // Using a static variable to avoid initializing the array - // each time the function is called. Moving the declaration on the - // top of the function slow downs other escaping strategies. - static $htmlspecialcharsCharsets = [ - 'ISO-8859-1' => true, 'ISO8859-1' => true, - 'ISO-8859-15' => true, 'ISO8859-15' => true, - 'utf-8' => true, 'UTF-8' => true, - 'CP866' => true, 'IBM866' => true, '866' => true, - 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, - '1251' => true, - 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, - 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, - 'BIG5' => true, '950' => true, - 'GB2312' => true, '936' => true, - 'BIG5-HKSCS' => true, - 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, - 'EUC-JP' => true, 'EUCJP' => true, - 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, - ]; - - if (isset($htmlspecialcharsCharsets[$charset])) { - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } + if (null === $charset) { + $charset = $env->getCharset(); + } - if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { - // cache the lowercase variant for future iterations - $htmlspecialcharsCharsets[$charset] = true; + switch ($strategy) { + case 'html': + // see https://www.php.net/htmlspecialchars + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = [ + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ]; - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } - $string = twig_convert_encoding($string, 'UTF-8', $charset); - $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; - return iconv('UTF-8', $charset, $string); + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } - case 'js': - // escape all non-alphanumeric characters - // into their \x or \uHHHH representations - if ('UTF-8' !== $charset) { $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { - $char = $matches[0]; - - /* - * A few characters have short escape sequences in JSON and JavaScript. - * Escape sequences supported only by JavaScript, not JSON, are omitted. - * \" is also supported but omitted, because the resulting string is not HTML safe. - */ - static $shortMap = [ - '\\' => '\\\\', - '/' => '\\/', - "\x08" => '\b', - "\x0C" => '\f', - "\x0A" => '\n', - "\x0D" => '\r', - "\x09" => '\t', - ]; + return iconv('UTF-8', $charset, $string); - if (isset($shortMap[$char])) { - return $shortMap[$char]; + case 'js': + // escape all non-alphanumeric characters + // into their \x or \uHHHH representations + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); } - $codepoint = mb_ord($char, 'UTF-8'); - if (0x10000 > $codepoint) { - return sprintf('\u%04X', $codepoint); + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); } - // Split characters outside the BMP into surrogate pairs - // https://tools.ietf.org/html/rfc2781.html#section-2.1 - $u = $codepoint - 0x10000; - $high = 0xD800 | ($u >> 10); - $low = 0xDC00 | ($u & 0x3FF); + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { + $char = $matches[0]; - return sprintf('\u%04X\u%04X', $high, $low); - }, $string); + /* + * A few characters have short escape sequences in JSON and JavaScript. + * Escape sequences supported only by JavaScript, not JSON, are omitted. + * \" is also supported but omitted, because the resulting string is not HTML safe. + */ + static $shortMap = [ + '\\' => '\\\\', + '/' => '\\/', + "\x08" => '\b', + "\x0C" => '\f', + "\x0A" => '\n', + "\x0D" => '\r', + "\x09" => '\t', + ]; - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + if (isset($shortMap[$char])) { + return $shortMap[$char]; + } - return $string; + $codepoint = mb_ord($char, 'UTF-8'); + if (0x10000 > $codepoint) { + return sprintf('\u%04X', $codepoint); + } - case 'css': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + // Split characters outside the BMP into surrogate pairs + // https://tools.ietf.org/html/rfc2781.html#section-2.1 + $u = $codepoint - 0x10000; + $high = 0xD800 | ($u >> 10); + $low = 0xDC00 | ($u & 0x3FF); - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } + return sprintf('\u%04X\u%04X', $high, $low); + }, $string); - $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { - $char = $matches[0]; + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } - return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); - }, $string); + return $string; - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + case 'css': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - return $string; + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } - case 'html_attr': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { + $char = $matches[0]; - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } + return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); + }, $string); - $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { - /** - * This function is adapted from code coming from Zend Framework. - * - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) - * @license https://framework.zend.com/license/new-bsd New BSD License - */ - $chr = $matches[0]; - $ord = \ord($chr); - - /* - * The following replaces characters undefined in HTML with the - * hex entity for the Unicode replacement character. - */ - if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { - return '�'; + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); } - /* - * Check if the current character to escape has a name entity we should - * replace it with while grabbing the hex value of the character. - */ - if (1 === \strlen($chr)) { - /* - * While HTML supports far more named entities, the lowest common denominator - * has become HTML5's XML Serialisation which is restricted to the those named - * entities that XML supports. Using HTML entities would result in this error: - * XML Parsing Error: undefined entity - */ - static $entityMap = [ - 34 => '"', /* quotation mark */ - 38 => '&', /* ampersand */ - 60 => '<', /* less-than sign */ - 62 => '>', /* greater-than sign */ - ]; + return $string; - if (isset($entityMap[$ord])) { - return $entityMap[$ord]; - } + case 'html_attr': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - return sprintf('&#x%02X;', $ord); + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); } - /* - * Per OWASP recommendations, we'll use hex entities for any other - * characters where a named entity does not exist. - */ - return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); - }, $string); + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { + /** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License + */ + $chr = $matches[0]; + $ord = \ord($chr); - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + /* + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { + return '�'; + } - return $string; + /* + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (1 === \strlen($chr)) { + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = [ + 34 => '"', /* quotation mark */ + 38 => '&', /* ampersand */ + 60 => '<', /* less-than sign */ + 62 => '>', /* greater-than sign */ + ]; + + if (isset($entityMap[$ord])) { + return $entityMap[$ord]; + } + + return sprintf('&#x%02X;', $ord); + } - case 'url': - return rawurlencode($string); + /* + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } - default: - $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); - if (\array_key_exists($strategy, $escapers)) { - return $escapers[$strategy]($env, $string, $charset); - } + return $string; - $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); + case 'url': + return rawurlencode($string); - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); - } -} + default: + $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); + if (\array_key_exists($strategy, $escapers)) { + return $escapers[$strategy]($env, $string, $charset); + } -/** - * @internal - */ -function twig_escape_filter_is_safe(Node $filterArgs) -{ - foreach ($filterArgs as $arg) { - if ($arg instanceof ConstantExpression) { - return [$arg->getAttribute('value')]; - } + $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); - return []; + throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); + } } - - return ['html']; -} } diff --git a/src/Resources/escaper.php b/src/Resources/escaper.php new file mode 100644 index 00000000000..cf038645f8e --- /dev/null +++ b/src/Resources/escaper.php @@ -0,0 +1,35 @@ +createMock(LoaderInterface::class)); foreach ($this->htmlSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'html'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'html'), 'Failed to escape: '.$key); } } @@ -170,7 +170,7 @@ public function testHtmlAttributeEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->htmlAttrSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'html_attr'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'html_attr'), 'Failed to escape: '.$key); } } @@ -178,7 +178,7 @@ public function testJavascriptEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->jsSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'js'), 'Failed to escape: '.$key); } } @@ -189,7 +189,7 @@ public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding() try { mb_internal_encoding('ISO-8859-1'); foreach ($this->jsSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'js'), 'Failed to escape: '.$key); } } finally { if (false !== $previousInternalEncoding) { @@ -201,40 +201,40 @@ public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding() public function testJavascriptEscapingReturnsStringIfZeroLength() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('', twig_escape_filter($twig, '', 'js')); + $this->assertEquals('', EscaperExtension::escape($twig, '', 'js')); } public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('123', twig_escape_filter($twig, '123', 'js')); + $this->assertEquals('123', EscaperExtension::escape($twig, '123', 'js')); } public function testCssEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->cssSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'css'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'css'), 'Failed to escape: '.$key); } } public function testCssEscapingReturnsStringIfZeroLength() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('', twig_escape_filter($twig, '', 'css')); + $this->assertEquals('', EscaperExtension::escape($twig, '', 'css')); } public function testCssEscapingReturnsStringIfContainsOnlyDigits() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('123', twig_escape_filter($twig, '123', 'css')); + $this->assertEquals('123', EscaperExtension::escape($twig, '123', 'css')); } public function testUrlEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->urlSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'url'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'url'), 'Failed to escape: '.$key); } } @@ -296,15 +296,15 @@ public function testJavascriptEscapingEscapesOwaspRecommendedRanges() || $chr >= 0x41 && $chr <= 0x5A || $chr >= 0x61 && $chr <= 0x7A) { $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'js')); } else { $literal = $this->codepointToUtf8($chr); if (\in_array($literal, $immune)) { - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'js')); } else { $this->assertNotEquals( $literal, - twig_escape_filter($twig, $literal, 'js'), + EscaperExtension::escape($twig, $literal, 'js'), "$literal should be escaped!"); } } @@ -320,15 +320,15 @@ public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges() || $chr >= 0x41 && $chr <= 0x5A || $chr >= 0x61 && $chr <= 0x7A) { $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'html_attr')); } else { $literal = $this->codepointToUtf8($chr); if (\in_array($literal, $immune)) { - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'html_attr')); } else { $this->assertNotEquals( $literal, - twig_escape_filter($twig, $literal, 'html_attr'), + EscaperExtension::escape($twig, $literal, 'html_attr'), "$literal should be escaped!"); } } @@ -344,12 +344,12 @@ public function testCssEscapingEscapesOwaspRecommendedRanges() || $chr >= 0x41 && $chr <= 0x5A || $chr >= 0x61 && $chr <= 0x7A) { $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'css')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'css')); } else { $literal = $this->codepointToUtf8($chr); $this->assertNotEquals( $literal, - twig_escape_filter($twig, $literal, 'css'), + EscaperExtension::escape($twig, $literal, 'css'), "$literal should be escaped!"); } } @@ -359,7 +359,7 @@ public function testUnknownCustomEscaper() { $this->expectException(RuntimeError::class); - twig_escape_filter(new Environment($this->createMock(LoaderInterface::class)), 'foo', 'bar'); + EscaperExtension::escape(new Environment($this->createMock(LoaderInterface::class)), 'foo', 'bar'); } /** @@ -370,7 +370,7 @@ public function testCustomEscaper($expected, $string, $strategy) $twig = new Environment($this->createMock(LoaderInterface::class)); $twig->getExtension(EscaperExtension::class)->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); - $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy)); + $this->assertSame($expected, EscaperExtension::escape($twig, $string, $strategy)); } public function provideCustomEscaperCases() @@ -389,8 +389,8 @@ public function testCustomEscapersOnMultipleEnvs() $env2 = new Environment($this->createMock(LoaderInterface::class)); $env2->getExtension(EscaperExtension::class)->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test1'); - $this->assertSame('fooUTF-8', twig_escape_filter($env1, 'foo', 'foo')); - $this->assertSame('fooUTF-81', twig_escape_filter($env2, 'foo', 'foo')); + $this->assertSame('fooUTF-8', EscaperExtension::escape($env1, 'foo', 'foo')); + $this->assertSame('fooUTF-81', EscaperExtension::escape($env2, 'foo', 'foo')); } /** @@ -401,8 +401,8 @@ public function testObjectEscaping(string $escapedHtml, string $escapedJs, array $obj = new Extension_TestClass(); $twig = new Environment($this->createMock(LoaderInterface::class)); $twig->getExtension('\Twig\Extension\EscaperExtension')->setSafeClasses($safeClasses); - $this->assertSame($escapedHtml, twig_escape_filter($twig, $obj, 'html', null, true)); - $this->assertSame($escapedJs, twig_escape_filter($twig, $obj, 'js', null, true)); + $this->assertSame($escapedHtml, EscaperExtension::escape($twig, $obj, 'html', null, true)); + $this->assertSame($escapedJs, EscaperExtension::escape($twig, $obj, 'js', null, true)); } public function provideObjectsForEscaping() diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index aa24d5fbe8f..8dd5e3bf7f8 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -13,6 +13,7 @@ use Twig\Extension\AbstractExtension; use Twig\Extension\DebugExtension; +use Twig\Extension\EscaperExtension; use Twig\Extension\SandboxExtension; use Twig\Extension\StringLoaderExtension; use Twig\Node\Expression\ConstantExpression; @@ -215,7 +216,7 @@ public function §Function($value) */ public function escape_and_nl2br($env, $value, $sep = '
') { - return $this->nl2br(twig_escape_filter($env, $value, 'html'), $sep); + return $this->nl2br(EscaperExtension::escape($env, $value, 'html'), $sep); } /** From 54d34b969b256ecd1c7aa3416205206d87686ef7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 20:01:42 +0100 Subject: [PATCH 09/10] Move functions for CoreExtension --- composer.json | 1 + extra/intl-extra/IntlExtension.php | 3 +- src/Extension/CoreExtension.php | 2478 +++++++++-------- src/Node/Expression/ArrayExpression.php | 2 +- src/Node/Expression/Binary/EqualBinary.php | 2 +- src/Node/Expression/Binary/GreaterBinary.php | 2 +- .../Expression/Binary/GreaterEqualBinary.php | 2 +- src/Node/Expression/Binary/HasEveryBinary.php | 2 +- src/Node/Expression/Binary/HasSomeBinary.php | 2 +- src/Node/Expression/Binary/InBinary.php | 2 +- src/Node/Expression/Binary/LessBinary.php | 2 +- .../Expression/Binary/LessEqualBinary.php | 2 +- src/Node/Expression/Binary/MatchesBinary.php | 2 +- src/Node/Expression/Binary/NotEqualBinary.php | 2 +- src/Node/Expression/Binary/NotInBinary.php | 2 +- src/Node/Expression/FunctionExpression.php | 3 +- src/Node/Expression/GetAttrExpression.php | 2 +- src/Node/Expression/MethodCallExpression.php | 2 +- src/Node/ForNode.php | 2 +- src/Node/IncludeNode.php | 4 +- src/Node/ModuleNode.php | 1 + src/Node/WithNode.php | 2 +- src/Resources/core.php | 497 ++++ src/Test/NodeTestCase.php | 2 +- tests/Extension/CoreTest.php | 39 +- tests/Extension/StringLoaderExtensionTest.php | 3 +- tests/Node/Expression/FilterTest.php | 10 +- tests/Node/Expression/FunctionTest.php | 2 +- tests/Node/ForTest.php | 8 +- tests/Node/IncludeTest.php | 6 +- tests/Node/ModuleTest.php | 9 +- tests/TemplateTest.php | 35 +- 32 files changed, 1865 insertions(+), 1268 deletions(-) create mode 100644 src/Resources/core.php diff --git a/composer.json b/composer.json index bceeedd328d..1e422dbbafe 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ }, "autoload": { "files": [ + "src/Resources/core.php", "src/Resources/debug.php", "src/Resources/escaper.php", "src/Resources/string_loader.php" diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 142d25e10c5..13d4a4e4778 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -21,6 +21,7 @@ use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; +use Twig\Extension\CoreExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -368,7 +369,7 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str */ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string { - $date = twig_date_converter($env, $date, $timezone); + $date = CoreExtension::dateConverter($env, $date, $timezone); $formatterTimezone = $timezone; if (null === $formatterTimezone) { diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 36aa8f10a7f..8c9d0d4feff 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -9,8 +9,14 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; use Twig\ExpressionParser; +use Twig\Extension\SandboxExtension; +use Twig\Markup; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Binary\BitwiseAndBinary; @@ -52,6 +58,9 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\TokenParser\ApplyTokenParser; use Twig\TokenParser\BlockTokenParser; use Twig\TokenParser\DeprecatedTokenParser; @@ -177,50 +186,50 @@ public function getFilters(): array { return [ // formatting filters - new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]), - new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]), - new TwigFilter('format', 'twig_sprintf'), - new TwigFilter('replace', 'twig_replace_filter'), - new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]), + new TwigFilter('date', [self::class, 'dateFormatFilter'], ['needs_environment' => true]), + new TwigFilter('date_modify', [self::class, 'dateModifyFilter'], ['needs_environment' => true]), + new TwigFilter('format', [self::class, 'sprintf']), + new TwigFilter('replace', [self::class, 'replaceFilter']), + new TwigFilter('number_format', [self::class, 'numberFormatFilter'], ['needs_environment' => true]), new TwigFilter('abs', 'abs'), - new TwigFilter('round', 'twig_round'), + new TwigFilter('round', [self::class, 'round']), // encoding - new TwigFilter('url_encode', 'twig_urlencode_filter'), + new TwigFilter('url_encode', [self::class, 'urlencodeFilter']), new TwigFilter('json_encode', 'json_encode'), - new TwigFilter('convert_encoding', 'twig_convert_encoding'), + new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), // string filters - new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]), - new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]), - new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]), - new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]), - new TwigFilter('striptags', 'twig_striptags'), - new TwigFilter('trim', 'twig_trim_filter'), - new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]), + new TwigFilter('title', [self::class, 'titleStringFilter'], ['needs_environment' => true]), + new TwigFilter('capitalize', [self::class, 'capitalizeStringFilter'], ['needs_environment' => true]), + new TwigFilter('upper', [self::class, 'upperFilter'], ['needs_environment' => true]), + new TwigFilter('lower', [self::class, 'lowerFilter'], ['needs_environment' => true]), + new TwigFilter('striptags', [self::class, 'striptags']), + new TwigFilter('trim', [self::class, 'trimFilter']), + new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html']]), // array helpers - new TwigFilter('join', 'twig_join_filter'), - new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]), - new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]), - new TwigFilter('merge', 'twig_array_merge'), - new TwigFilter('batch', 'twig_array_batch'), - new TwigFilter('column', 'twig_array_column'), - new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]), - new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]), - new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]), + new TwigFilter('join', [self::class, 'joinFilter']), + new TwigFilter('split', [self::class, 'splitFilter'], ['needs_environment' => true]), + new TwigFilter('sort', [self::class, 'sortFilter'], ['needs_environment' => true]), + new TwigFilter('merge', [self::class, 'arrayMerge']), + new TwigFilter('batch', [self::class, 'arrayBatch']), + new TwigFilter('column', [self::class, 'arrayColumn']), + new TwigFilter('filter', [self::class, 'arrayFilter'], ['needs_environment' => true]), + new TwigFilter('map', [self::class, 'arrayMap'], ['needs_environment' => true]), + new TwigFilter('reduce', [self::class, 'arrayReduce'], ['needs_environment' => true]), // string/array filters - new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]), - new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]), - new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]), - new TwigFilter('first', 'twig_first', ['needs_environment' => true]), - new TwigFilter('last', 'twig_last', ['needs_environment' => true]), + new TwigFilter('reverse', [self::class, 'reverseFilter'], ['needs_environment' => true]), + new TwigFilter('length', [self::class, 'lengthFilter'], ['needs_environment' => true]), + new TwigFilter('slice', [self::class, 'slice'], ['needs_environment' => true]), + new TwigFilter('first', [self::class, 'first'], ['needs_environment' => true]), + new TwigFilter('last', [self::class, 'last'], ['needs_environment' => true]), // iteration and runtime - new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]), - new TwigFilter('keys', 'twig_get_array_keys_filter'), + new TwigFilter('default', [self::class, 'defaultFilter'], ['node_class' => DefaultFilter::class]), + new TwigFilter('keys', [self::class, 'getArrayKeysFilter']), ]; } @@ -230,12 +239,12 @@ public function getFunctions(): array new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), - new TwigFunction('constant', 'twig_constant'), - new TwigFunction('cycle', 'twig_cycle'), - new TwigFunction('random', 'twig_random', ['needs_environment' => true]), - new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]), - new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), - new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('constant', [self::class, 'constant']), + new TwigFunction('cycle', [self::class, 'cycle']), + new TwigFunction('random', [self::class, 'random'], ['needs_environment' => true]), + new TwigFunction('date', [self::class, 'dateConverter'], ['needs_environment' => true]), + new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), + new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), ]; } @@ -250,7 +259,7 @@ public function getTests(): array new TwigTest('null', null, ['node_class' => NullTest::class]), new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]), new TwigTest('constant', null, ['node_class' => ConstantTest::class]), - new TwigTest('empty', 'twig_test_empty'), + new TwigTest('empty', [self::class, 'testEmpty']), new TwigTest('iterable', 'is_iterable'), ]; } @@ -303,192 +312,213 @@ public function getOperators(): array ], ]; } -} -} - -namespace { - use Twig\Environment; - use Twig\Error\LoaderError; - use Twig\Error\RuntimeError; - use Twig\Extension\CoreExtension; - use Twig\Extension\SandboxExtension; - use Twig\Markup; - use Twig\Source; - use Twig\Template; - use Twig\TemplateWrapper; - -/** - * Cycles over a value. - * - * @param \ArrayAccess|array $values - * @param int $position The cycle position - * - * @return string The next value in the cycle - */ -function twig_cycle($values, $position) -{ - if (!\is_array($values) && !$values instanceof \ArrayAccess) { - return $values; - } - return $values[$position % \count($values)]; -} + /** + * Cycles over a value. + * + * @param \ArrayAccess|array $values + * @param int $position The cycle position + * + * @return string The next value in the cycle + * + * @internal + */ + public static function cycle($values, $position) + { + if (!\is_array($values) && !$values instanceof \ArrayAccess) { + return $values; + } -/** - * Returns a random value depending on the supplied parameter type: - * - a random item from a \Traversable or array - * - a random character from a string - * - a random integer between 0 and the integer parameter. - * - * @param \Traversable|array|int|float|string $values The values to pick a random item from - * @param int|null $max Maximum value used when $values is an int - * - * @return mixed A random value from the given sequence - * - * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) - */ -function twig_random(Environment $env, $values = null, $max = null) -{ - if (null === $values) { - return null === $max ? mt_rand() : mt_rand(0, (int) $max); + return $values[$position % \count($values)]; } - if (\is_int($values) || \is_float($values)) { - if (null === $max) { - if ($values < 0) { - $max = 0; - $min = $values; + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @return mixed A random value from the given sequence + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + * @internal + */ + public static function random(Environment $env, $values = null, $max = null) + { + if (null === $values) { + return null === $max ? mt_rand() : mt_rand(0, (int) $max); + } + + if (\is_int($values) || \is_float($values)) { + if (null === $max) { + if ($values < 0) { + $max = 0; + $min = $values; + } else { + $max = $values; + $min = 0; + } } else { - $max = $values; - $min = 0; + $min = $values; } - } else { - $min = $values; + + return mt_rand((int) $min, (int) $max); } - return mt_rand((int) $min, (int) $max); - } + if (\is_string($values)) { + if ('' === $values) { + return ''; + } - if (\is_string($values)) { - if ('' === $values) { - return ''; - } + $charset = $env->getCharset(); - $charset = $env->getCharset(); + if ('UTF-8' !== $charset) { + $values = self::convertEncoding($values, 'UTF-8', $charset); + } - if ('UTF-8' !== $charset) { - $values = twig_convert_encoding($values, 'UTF-8', $charset); + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = self::convertEncoding($value, $charset, 'UTF-8'); + } + } + } + + if (!is_iterable($values)) { + return $values; } - // unicode version of str_split() - // split at all positions, but not after the start and not before the end - $values = preg_split('/(? $value) { - $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); - } + if (0 === \count($values)) { + throw new RuntimeError('The random function cannot pick from an empty array.'); } - } - if (!is_iterable($values)) { - return $values; + return $values[array_rand($values, 1)]; } - $values = twig_to_array($values); + /** + * Converts a date to the given format. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string $date A date + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return string The formatted date + * + * @internal + */ + public static function dateFormatFilter(Environment $env, $date, $format = null, $timezone = null) + { + if (null === $format) { + $formats = $env->getExtension(self::class)->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof \DateInterval) { + return $date->format($format); + } - if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty array.'); + return self::dateConverter($env, $date, $timezone)->format($format); } - return $values[array_rand($values, 1)]; -} + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string $date A date + * @param string $modifier A modifier string + * + * @return \DateTimeInterface + * + * @internal + */ + public static function dateModifyFilter(Environment $env, $date, $modifier) + { + $date = self::dateConverter($env, $date, false); -/** - * Converts a date to the given format. - * - * {{ post.published_at|date("m/d/Y") }} - * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return string The formatted date - */ -function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) -{ - if (null === $format) { - $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); - $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + return $date->modify($modifier); } - if ($date instanceof \DateInterval) { - return $date->format($format); + /** + * Returns a formatted string. + * + * @param string|null $format + * @param ...$values + * + * @return string + * + * @internal + */ + public static function sprintf($format, ...$values) + { + return sprintf($format ?? '', ...$values); } - return twig_date_converter($env, $date, $timezone)->format($format); -} + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|null $date A date or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTimeInterface + * + * @internal + */ + public static function dateConverter(Environment $env, $date = null, $timezone = null) + { + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $env->getExtension(self::class)->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } -/** - * Returns a new date object modified. - * - * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} - * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string - * - * @return \DateTimeInterface - */ -function twig_date_modify_filter(Environment $env, $date, $modifier) -{ - $date = twig_date_converter($env, $date, false); + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } - return $date->modify($modifier); -} + if ($date instanceof \DateTimeInterface) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } -/** - * Returns a formatted string. - * - * @param string|null $format - * @param ...$values - * - * @return string - */ -function twig_sprintf($format, ...$values) -{ - return sprintf($format ?? '', ...$values); -} + return $date; + } -/** - * Converts an input to a \DateTime instance. - * - * {% if date(user.created_at) < date('+2days') %} - * {# do something #} - * {% endif %} - * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return \DateTimeInterface - */ -function twig_date_converter(Environment $env, $date = null, $timezone = null) -{ - // determine the timezone - if (false !== $timezone) { - if (null === $timezone) { - $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); - } elseif (!$timezone instanceof \DateTimeZone) { - $timezone = new \DateTimeZone($timezone); + if (null === $date || 'now' === $date) { + if (null === $date) { + $date = 'now'; + } + + return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(self::class)->getTimezone()); } - } - // immutable dates - if ($date instanceof \DateTimeImmutable) { - return false !== $timezone ? $date->setTimezone($timezone) : $date; - } + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@'.$date); + } else { + $date = new \DateTime($date, $env->getExtension(self::class)->getTimezone()); + } - if ($date instanceof \DateTimeInterface) { - $date = clone $date; if (false !== $timezone) { $date->setTimezone($timezone); } @@ -496,993 +526,1068 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) return $date; } - if (null === $date || 'now' === $date) { - if (null === $date) { - $date = 'now'; + /** + * Replaces strings within a string. + * + * @param string|null $str String to replace in + * @param array|\Traversable $from Replace values + * + * @return string + * + * @internal + */ + public static function replaceFilter($str, $from) + { + if (!is_iterable($from)) { + throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); } - return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); - } - - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { - $date = new \DateTime('@'.$date); - } else { - $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); - } - - if (false !== $timezone) { - $date->setTimezone($timezone); - } - - return $date; -} - -/** - * Replaces strings within a string. - * - * @param string|null $str String to replace in - * @param array|\Traversable $from Replace values - * - * @return string - */ -function twig_replace_filter($str, $from) -{ - if (!is_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + return strtr($str ?? '', self::toArray($from)); } - return strtr($str ?? '', twig_to_array($from)); -} + /** + * Rounds a number. + * + * @param int|float|string|null $value The value to round + * @param int|float $precision The rounding precision + * @param string $method The method to use for rounding + * + * @return int|float The rounded number + * + * @internal + */ + public static function round($value, $precision = 0, $method = 'common') + { + $value = (float) $value; -/** - * Rounds a number. - * - * @param int|float|string|null $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding - * - * @return int|float The rounded number - */ -function twig_round($value, $precision = 0, $method = 'common') -{ - $value = (float) $value; + if ('common' === $method) { + return round($value, $precision); + } - if ('common' === $method) { - return round($value, $precision); - } + if ('ceil' !== $method && 'floor' !== $method) { + throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + } - if ('ceil' !== $method && 'floor' !== $method) { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + return $method($value * 10 ** $precision) / 10 ** $precision; } - return $method($value * 10 ** $precision) / 10 ** $precision; -} + /** + * Number format filter. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int $decimal the number of decimal points to display + * @param string $decimalPoint the character(s) to use for the decimal point + * @param string $thousandSep the character(s) to use for the thousands separator + * + * @return string The formatted number + * + * @internal + */ + public static function numberFormatFilter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) + { + $defaults = $env->getExtension(self::class)->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } -/** - * Number format filter. - * - * All of the formatting options can be left null, in that case the defaults will - * be used. Supplying any of the parameters will override the defaults set in the - * environment object. - * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - * - * @return string The formatted number - */ -function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) -{ - $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); - if (null === $decimal) { - $decimal = $defaults[0]; - } + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } - if (null === $decimalPoint) { - $decimalPoint = $defaults[1]; - } + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } - if (null === $thousandSep) { - $thousandSep = $defaults[2]; + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); } - return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); -} + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array|null $url A URL or an array of query parameters + * + * @return string The URL encoded value + * + * @internal + */ + public static function urlencodeFilter($url) + { + if (\is_array($url)) { + return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + } -/** - * URL encodes (RFC 3986) a string as a path segment or an array as a query string. - * - * @param string|array|null $url A URL or an array of query parameters - * - * @return string The URL encoded value - */ -function twig_urlencode_filter($url) -{ - if (\is_array($url)) { - return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + return rawurlencode($url ?? ''); } - return rawurlencode($url ?? ''); -} + /** + * Merges any number of arrays or Traversable objects. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} + * + * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge + * + * @return array The merged array + * + * @internal + */ + public static function arrayMerge(...$arrays) + { + $result = []; -/** - * Merges any number of arrays or Traversable objects. - * - * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} - * - * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} - * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} - * - * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge - * - * @return array The merged array - */ -function twig_array_merge(...$arrays) -{ - $result = []; + foreach ($arrays as $argNumber => $array) { + if (!is_iterable($array)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + } - foreach ($arrays as $argNumber => $array) { - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + $result = array_merge($result, self::toArray($array)); } - $result = array_merge($result, twig_to_array($array)); + return $result; } - return $result; -} + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + * + * @internal + */ + public static function slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } + + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } -/** - * Slices a variable. - * - * @param mixed $item A variable - * @param int $start Start of the slice - * @param int $length Size of the slice - * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) - * - * @return mixed The sliced variable - */ -function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - while ($item instanceof \IteratorAggregate) { - $item = $item->getIterator(); + $item = iterator_to_array($item, $preserveKeys); } - if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { - try { - return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); - } catch (\OutOfBoundsException $e) { - return []; - } + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); } - $item = iterator_to_array($item, $preserveKeys); + return mb_substr((string) $item, $start, $length, $env->getCharset()); } - if (\is_array($item)) { - return \array_slice($item, $start, $length, $preserveKeys); + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + * + * @internal + */ + public static function first(Environment $env, $item) + { + $elements = self::slice($env, $item, 0, 1, false); + + return \is_string($elements) ? $elements : current($elements); } - return mb_substr((string) $item, $start, $length, $env->getCharset()); -} + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + * + * @internal + */ + public static function last(Environment $env, $item) + { + $elements = self::slice($env, $item, -1, 1, false); -/** - * Returns the first element of the item. - * - * @param mixed $item A variable - * - * @return mixed The first element of the item - */ -function twig_first(Environment $env, $item) -{ - $elements = twig_slice($env, $item, 0, 1, false); + return \is_string($elements) ? $elements : current($elements); + } - return \is_string($elements) ? $elements : current($elements); -} + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param array $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @return string The concatenated string + * + * @internal + */ + public static function joinFilter($value, $glue = '', $and = null) + { + if (!is_iterable($value)) { + $value = (array) $value; + } -/** - * Returns the last element of the item. - * - * @param mixed $item A variable - * - * @return mixed The last element of the item - */ -function twig_last(Environment $env, $item) -{ - $elements = twig_slice($env, $item, -1, 1, false); + $value = self::toArray($value, false); - return \is_string($elements) ? $elements : current($elements); -} + if (0 === \count($value)) { + return ''; + } -/** - * Joins the values to a string. - * - * The separators between elements are empty strings per default, you can define them with the optional parameters. - * - * {{ [1, 2, 3]|join(', ', ' and ') }} - * {# returns 1, 2 and 3 #} - * - * {{ [1, 2, 3]|join('|') }} - * {# returns 1|2|3 #} - * - * {{ [1, 2, 3]|join }} - * {# returns 123 #} - * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair - * - * @return string The concatenated string - */ -function twig_join_filter($value, $glue = '', $and = null) -{ - if (!is_iterable($value)) { - $value = (array) $value; - } + if (null === $and || $and === $glue) { + return implode($glue, $value); + } - $value = twig_to_array($value, false); + if (1 === \count($value)) { + return $value[0]; + } - if (0 === \count($value)) { - return ''; + return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; } - if (null === $and || $and === $glue) { - return implode($glue, $value); - } + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string|null $value A string + * @param string $delimiter The delimiter + * @param int $limit The limit + * + * @return array The split string as an array + * + * @internal + */ + public static function splitFilter(Environment $env, $value, $delimiter, $limit = null) + { + $value = $value ?? ''; - if (1 === \count($value)) { - return $value[0]; - } + if ('' !== $delimiter) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } - return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; -} + if ($limit <= 1) { + return preg_split('/(?getCharset()); + if ($length < $limit) { + return [$value]; + } - if ('' !== $delimiter) { - return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); - } + $r = []; + for ($i = 0; $i < $length; $i += $limit) { + $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + } - if ($limit <= 1) { - return preg_split('/(?getCharset()); - if ($length < $limit) { - return [$value]; - } + // The '_default' filter is used internally to avoid using the ternary operator + // which costs a lot for big contexts (before PHP 5.4). So, on average, + // a function call is cheaper. + /** + * @internal + */ + public static function defaultFilter($value, $default = '') + { + if (self::testEmpty($value)) { + return $default; + } - $r = []; - for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + return $value; } - return $r; -} - -// The '_default' filter is used internally to avoid using the ternary operator -// which costs a lot for big contexts (before PHP 5.4). So, on average, -// a function call is cheaper. -/** - * @internal - */ -function _twig_default_filter($value, $default = '') -{ - if (twig_test_empty($value)) { - return $default; - } + /** + * Returns the keys for the given array. + * + * It is useful when you want to iterate over the keys of an array: + * + * {% for key in array|keys %} + * {# ... #} + * {% endfor %} + * + * @param array $array An array + * + * @return array The keys + * + * @internal + */ + public static function getArrayKeysFilter($array) + { + if ($array instanceof \Traversable) { + while ($array instanceof \IteratorAggregate) { + $array = $array->getIterator(); + } - return $value; -} + $keys = []; + if ($array instanceof \Iterator) { + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } -/** - * Returns the keys for the given array. - * - * It is useful when you want to iterate over the keys of an array: - * - * {% for key in array|keys %} - * {# ... #} - * {% endfor %} - * - * @param array $array An array - * - * @return array The keys - */ -function twig_get_array_keys_filter($array) -{ - if ($array instanceof \Traversable) { - while ($array instanceof \IteratorAggregate) { - $array = $array->getIterator(); - } + return $keys; + } - $keys = []; - if ($array instanceof \Iterator) { - $array->rewind(); - while ($array->valid()) { - $keys[] = $array->key(); - $array->next(); + foreach ($array as $key => $item) { + $keys[] = $key; } return $keys; } - foreach ($array as $key => $item) { - $keys[] = $key; + if (!\is_array($array)) { + return []; } - return $keys; - } - - if (!\is_array($array)) { - return []; + return array_keys($array); } - return array_keys($array); -} + /** + * Reverses a variable. + * + * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + * + * @internal + */ + public static function reverseFilter(Environment $env, $item, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); + } -/** - * Reverses a variable. - * - * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not - * - * @return mixed The reversed input - */ -function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - return array_reverse(iterator_to_array($item), $preserveKeys); - } + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } - if (\is_array($item)) { - return array_reverse($item, $preserveKeys); - } + $string = (string) $item; - $string = (string) $item; + $charset = $env->getCharset(); - $charset = $env->getCharset(); + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, 'UTF-8', $charset); + } - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + preg_match_all('/./us', $string, $matches); - preg_match_all('/./us', $string, $matches); + $string = implode('', array_reverse($matches[0])); - $string = implode('', array_reverse($matches[0])); + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, $charset, 'UTF-8'); + } - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, $charset, 'UTF-8'); + return $string; } - return $string; -} + /** + * Sorts an array. + * + * @param array|\Traversable $array + * + * @return array + * + * @internal + */ + public static function sortFilter(Environment $env, $array, $arrow = null) + { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + } -/** - * Sorts an array. - * - * @param array|\Traversable $array - * - * @return array - */ -function twig_sort_filter(Environment $env, $array, $arrow = null) -{ - if ($array instanceof \Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); - } + if (null !== $arrow) { + self::checkArrowInSandbox($env, $arrow, 'sort', 'filter'); - if (null !== $arrow) { - twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter'); + uasort($array, $arrow); + } else { + asort($array); + } - uasort($array, $arrow); - } else { - asort($array); + return $array; } - return $array; -} + /** + * @internal + */ + public static function inFilter($value, $compare) + { + if ($value instanceof Markup) { + $value = (string) $value; + } + if ($compare instanceof Markup) { + $compare = (string) $compare; + } -/** - * @internal - */ -function twig_in_filter($value, $compare) -{ - if ($value instanceof Markup) { - $value = (string) $value; - } - if ($compare instanceof Markup) { - $compare = (string) $compare; - } + if (\is_string($compare)) { + if (\is_string($value) || \is_int($value) || \is_float($value)) { + return '' === $value || str_contains($compare, (string) $value); + } - if (\is_string($compare)) { - if (\is_string($value) || \is_int($value) || \is_float($value)) { - return '' === $value || str_contains($compare, (string) $value); + return false; } - return false; - } - - if (!is_iterable($compare)) { - return false; - } + if (!is_iterable($compare)) { + return false; + } - if (\is_object($value) || \is_resource($value)) { - if (!\is_array($compare)) { - foreach ($compare as $item) { - if ($item === $value) { - return true; + if (\is_object($value) || \is_resource($value)) { + if (!\is_array($compare)) { + foreach ($compare as $item) { + if ($item === $value) { + return true; + } } + + return false; } - return false; + return \in_array($value, $compare, true); } - return \in_array($value, $compare, true); - } - - foreach ($compare as $item) { - if (0 === twig_compare($value, $item)) { - return true; + foreach ($compare as $item) { + if (0 === self::compare($value, $item)) { + return true; + } } - } - - return false; -} -/** - * Compares two values using a more strict version of the PHP non-strict comparison operator. - * - * @see https://wiki.php.net/rfc/string_to_number_comparison - * @see https://wiki.php.net/rfc/trailing_whitespace_numerics - * - * @internal - */ -function twig_compare($a, $b) -{ - // int <=> string - if (\is_int($a) && \is_string($b)) { - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - if ((int) $bTrim == $bTrim) { - return $a <=> (int) $bTrim; - } else { - return (float) $a <=> (float) $bTrim; - } + return false; } - if (\is_string($a) && \is_int($b)) { - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + + /** + * Compares two values using a more strict version of the PHP non-strict comparison operator. + * + * @see https://wiki.php.net/rfc/string_to_number_comparison + * @see https://wiki.php.net/rfc/trailing_whitespace_numerics + * + * @internal + */ + public static function compare($a, $b) + { + // int <=> string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + if ((int) $bTrim == $bTrim) { + return $a <=> (int) $bTrim; + } else { + return (float) $a <=> (float) $bTrim; + } } - if ((int) $aTrim == $aTrim) { - return (int) $aTrim <=> $b; - } else { - return (float) $aTrim <=> (float) $b; + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + if ((int) $aTrim == $aTrim) { + return (int) $aTrim <=> $b; + } else { + return (float) $aTrim <=> (float) $b; + } } - } - // float <=> string - if (\is_float($a) && \is_string($b)) { - if (is_nan($a)) { - return 1; + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + + return $a <=> (float) $bTrim; } - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + + return (float) $aTrim <=> $b; } - return $a <=> (float) $bTrim; + // fallback to <=> + return $a <=> $b; } - if (\is_string($a) && \is_float($b)) { - if (is_nan($b)) { - return 1; + + /** + * @return int + * + * @throws RuntimeError When an invalid pattern is used + * + * @internal + */ + public static function matches(string $regexp, ?string $str) + { + set_error_handler(function ($t, $m) use ($regexp) { + throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); + try { + return preg_match($regexp, $str ?? ''); + } finally { + restore_error_handler(); } - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + } + + /** + * Returns a trimmed string. + * + * @param string|null $string + * @param string|null $characterMask + * @param string $side + * + * @return string + * + * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') + * + * @internal + */ + public static function trimFilter($string, $characterMask = null, $side = 'both') + { + if (null === $characterMask) { + $characterMask = " \t\n\r\0\x0B"; } - return (float) $aTrim <=> $b; + switch ($side) { + case 'both': + return trim($string ?? '', $characterMask); + case 'left': + return ltrim($string ?? '', $characterMask); + case 'right': + return rtrim($string ?? '', $characterMask); + default: + throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + } } - // fallback to <=> - return $a <=> $b; -} - -/** - * @return int - * - * @throws RuntimeError When an invalid pattern is used - */ -function twig_matches(string $regexp, ?string $str) -{ - set_error_handler(function ($t, $m) use ($regexp) { - throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); - }); - try { - return preg_match($regexp, $str ?? ''); - } finally { - restore_error_handler(); + /** + * Inserts HTML line breaks before all newlines in a string. + * + * @param string|null $string + * + * @return string + * + * @internal + */ + public static function nl2br($string) + { + return nl2br($string ?? ''); } -} -/** - * Returns a trimmed string. - * - * @param string|null $string - * @param string|null $characterMask - * @param string $side - * - * @return string - * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') - */ -function twig_trim_filter($string, $characterMask = null, $side = 'both') -{ - if (null === $characterMask) { - $characterMask = " \t\n\r\0\x0B"; + /** + * Removes whitespaces between HTML tags. + * + * @param string|null $string + * + * @return string + * + * @internal + */ + public static function spaceless($content) + { + return trim(preg_replace('/>\s+<', $content ?? '')); } - switch ($side) { - case 'both': - return trim($string ?? '', $characterMask); - case 'left': - return ltrim($string ?? '', $characterMask); - case 'right': - return rtrim($string ?? '', $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + /** + * @param string|null $string + * @param string $to + * @param string $from + * + * @return string + * + * @internal + */ + public static function convertEncoding($string, $to, $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string ?? ''); } -} -/** - * Inserts HTML line breaks before all newlines in a string. - * - * @param string|null $string - * - * @return string - */ -function twig_nl2br($string) -{ - return nl2br($string ?? ''); -} + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @return int The length of the value + * + * @internal + */ + public static function lengthFilter(Environment $env, $thing) + { + if (null === $thing) { + return 0; + } -/** - * Removes whitespaces between HTML tags. - * - * @param string|null $string - * - * @return string - */ -function twig_spaceless($content) -{ - return trim(preg_replace('/>\s+<', $content ?? '')); -} + if (\is_scalar($thing)) { + return mb_strlen($thing, $env->getCharset()); + } -/** - * @param string|null $string - * @param string $to - * @param string $from - * - * @return string - */ -function twig_convert_encoding($string, $to, $from) -{ - if (!\function_exists('iconv')) { - throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); - } + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } - return iconv($from, $to, $string ?? ''); -} + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } -/** - * Returns the length of a variable. - * - * @param mixed $thing A variable - * - * @return int The length of the value - */ -function twig_length_filter(Environment $env, $thing) -{ - if (null === $thing) { - return 0; - } + if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + return mb_strlen((string) $thing, $env->getCharset()); + } - if (\is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); + return 1; } - if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { - return \count($thing); + /** + * Converts a string to uppercase. + * + * @param string|null $string A string + * + * @return string The uppercased string + * + * @internal + */ + public static function upperFilter(Environment $env, $string) + { + return mb_strtoupper($string ?? '', $env->getCharset()); } - if ($thing instanceof \Traversable) { - return iterator_count($thing); + /** + * Converts a string to lowercase. + * + * @param string|null $string A string + * + * @return string The lowercased string + * + * @internal + */ + public static function lowerFilter(Environment $env, $string) + { + return mb_strtolower($string ?? '', $env->getCharset()); } - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { - return mb_strlen((string) $thing, $env->getCharset()); + /** + * Strips HTML and PHP tags from a string. + * + * @param string|null $string + * @param string[]|string|null $string + * + * @return string + * + * @internal + */ + public static function striptags($string, $allowable_tags = null) + { + return strip_tags($string ?? '', $allowable_tags); } - return 1; -} - -/** - * Converts a string to uppercase. - * - * @param string|null $string A string - * - * @return string The uppercased string - */ -function twig_upper_filter(Environment $env, $string) -{ - return mb_strtoupper($string ?? '', $env->getCharset()); -} - -/** - * Converts a string to lowercase. - * - * @param string|null $string A string - * - * @return string The lowercased string - */ -function twig_lower_filter(Environment $env, $string) -{ - return mb_strtolower($string ?? '', $env->getCharset()); -} - -/** - * Strips HTML and PHP tags from a string. - * - * @param string|null $string - * @param string[]|string|null $string - * - * @return string - */ -function twig_striptags($string, $allowable_tags = null) -{ - return strip_tags($string ?? '', $allowable_tags); -} + /** + * Returns a titlecased string. + * + * @param string|null $string A string + * + * @return string The titlecased string + * + * @internal + */ + public static function titleStringFilter(Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); + } -/** - * Returns a titlecased string. - * - * @param string|null $string A string - * - * @return string The titlecased string - */ -function twig_title_string_filter(Environment $env, $string) -{ - if (null !== $charset = $env->getCharset()) { - return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); + return ucwords(strtolower($string ?? '')); } - return ucwords(strtolower($string ?? '')); -} - -/** - * Returns a capitalized string. - * - * @param string|null $string A string - * - * @return string The capitalized string - */ -function twig_capitalize_string_filter(Environment $env, $string) -{ - $charset = $env->getCharset(); + /** + * Returns a capitalized string. + * + * @param string|null $string A string + * + * @return string The capitalized string + * + * @internal + */ + public static function capitalizeStringFilter(Environment $env, $string) + { + $charset = $env->getCharset(); - return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); -} + return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); + } -/** - * @internal - */ -function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) -{ - if (!method_exists($template, $method)) { - $parent = $template; - while ($parent = $parent->getParent($context)) { - if (method_exists($parent, $method)) { - return $parent->$method(...$args); + /** + * @internal + */ + public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) + { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); + } } + + throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + return $template->$method(...$args); } - return $template->$method(...$args); -} + /** + * @internal + */ + public static function ensureTraversable($seq) + { + if (is_iterable($seq)) { + return $seq; + } -/** - * @internal - */ -function twig_ensure_traversable($seq) -{ - if (is_iterable($seq)) { - return $seq; + return []; } - return []; -} + /** + * @internal + */ + public static function toArray($seq, $preserveKeys = true) + { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } -/** - * @internal - */ -function twig_to_array($seq, $preserveKeys = true) -{ - if ($seq instanceof \Traversable) { - return iterator_to_array($seq, $preserveKeys); - } + if (!\is_array($seq)) { + return $seq; + } - if (!\is_array($seq)) { - return $seq; + return $preserveKeys ? $seq : array_values($seq); } - return $preserveKeys ? $seq : array_values($seq); -} + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is empty, false otherwise + * + * @internal + */ + public static function testEmpty($value) + { + if ($value instanceof \Countable) { + return 0 === \count($value); + } + + if ($value instanceof \Traversable) { + return !iterator_count($value); + } -/** - * Checks if a variable is empty. - * - * {# evaluates to true if the foo variable is null, false, or the empty string #} - * {% if foo is empty %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is empty, false otherwise - */ -function twig_test_empty($value) -{ - if ($value instanceof \Countable) { - return 0 === \count($value); - } + if (\is_object($value) && method_exists($value, '__toString')) { + return '' === (string) $value; + } - if ($value instanceof \Traversable) { - return !iterator_count($value); + return '' === $value || false === $value || null === $value || [] === $value; } - if (\is_object($value) && method_exists($value, '__toString')) { - return '' === (string) $value; + /** + * Checks if a variable is traversable. + * + * {# evaluates to true if the foo variable is an array or a traversable object #} + * {% if foo is iterable %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is traversable + * + * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) + * + * @internal + */ + public static function testIterable($value) + { + return is_iterable($value); } - return '' === $value || false === $value || null === $value || [] === $value; -} - -/** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - * - * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) - */ -function twig_test_iterable($value) -{ - return is_iterable($value); -} + /** + * Renders a template. + * + * @param array $context + * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @return string The rendered template + * + * @internal + */ + public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) + { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } -/** - * Renders a template. - * - * @param array $context - * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not - * - * @return string The rendered template - */ -function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) -{ - $alreadySandboxed = false; - $sandbox = null; - if ($withContext) { - $variables = array_merge($context, $variables); - } + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } - if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { - $sandbox = $env->getExtension(SandboxExtension::class); - 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(); + } + } } - 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 { + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + } + + return $loaded ? $loaded->render($variables) : ''; + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); } } } - try { - $loaded = null; + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @return string The template source + * + * @internal + */ + public static function source(Environment $env, $name, $ignoreMissing = false) + { + $loader = $env->getLoader(); try { - $loaded = $env->resolveTemplate($template); + return $loader->getSourceContext($name)->getCode(); } catch (LoaderError $e) { if (!$ignoreMissing) { throw $e; } } - - return $loaded ? $loaded->render($variables) : ''; - } finally { - if ($isSandboxed && !$alreadySandboxed) { - $sandbox->disableSandbox(); - } } -} -/** - * Returns a template content without rendering it. - * - * @param string $name The template name - * @param bool $ignoreMissing Whether to ignore missing templates or not - * - * @return string The template source - */ -function twig_source(Environment $env, $name, $ignoreMissing = false) -{ - $loader = $env->getLoader(); - try { - return $loader->getSourceContext($name)->getCode(); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; - } - } -} + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return string + * + * @internal + */ + public static function constant($constant, $object = null) + { + if (null !== $object) { + if ('class' === $constant) { + return \get_class($object); + } -/** - * Provides the ability to get constants from instances as well as class/global constants. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return string - */ -function twig_constant($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return \get_class($object); + $constant = \get_class($object).'::'.$constant; } - $constant = \get_class($object).'::'.$constant; - } + if (!\defined($constant)) { + throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); + } - if (!\defined($constant)) { - throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); + return \constant($constant); } - return \constant($constant); -} + /** + * Checks if a constant exists. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return bool + * + * @internal + */ + public static function constantIsDefined($constant, $object = null) + { + if (null !== $object) { + if ('class' === $constant) { + return true; + } -/** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return bool - */ -function twig_constant_is_defined($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return true; + $constant = \get_class($object).'::'.$constant; } - $constant = \get_class($object).'::'.$constant; + return \defined($constant); } - return \defined($constant); -} - -/** - * Batches item. - * - * @param array $items An array of items - * @param int $size The size of the batch - * @param mixed $fill A value used to fill missing items - * - * @return array - */ -function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) -{ - if (!is_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); - } + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @return array + * + * @internal + */ + public static function arrayBatch($items, $size, $fill = null, $preserveKeys = true) + { + if (!is_iterable($items)) { + throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + } - $size = ceil($size); + $size = ceil($size); - $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); + $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys); - if (null !== $fill && $result) { - $last = \count($result) - 1; - if ($fillCount = $size - \count($result[$last])) { - for ($i = 0; $i < $fillCount; ++$i) { - $result[$last][] = $fill; + if (null !== $fill && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } } } - } - return $result; -} + return $result; + } -/** - * Returns the attribute value for a given array/object. - * - * @param mixed $object The object or array from where to get the item - * @param mixed $item The item to get from the array or object - * @param array $arguments An array of arguments to pass if the item is an object method - * @param string $type The type of attribute (@see \Twig\Template constants) - * @param bool $isDefinedTest Whether this is only a defined check - * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not - * @param int $lineno The template line where the attribute was called - * - * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true - * - * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false - * - * @internal - */ -function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) -{ - // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \Twig\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + { + // array + if (/* Template::METHOD_CALL */ 'method' !== $type) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) + || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) + ) { + if ($isDefinedTest) { + return true; + } - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { - if ($isDefinedTest) { - return true; + return $object[$arrayItem]; } - return $object[$arrayItem]; + if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof \ArrayAccess) { + $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + } elseif (\is_object($object)) { + $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + } elseif (\is_array($object)) { + if (empty($object)) { + $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); + } else { + $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { + if (null === $object) { + $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + } elseif (null === $object) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } } - if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if (!\is_object($object)) { if ($isDefinedTest) { return false; } @@ -1491,260 +1596,245 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar return; } - if ($object instanceof ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); - } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + if (null === $object) { + $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); } elseif (\is_array($object)) { - if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); - } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); - } - } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { - if (null === $object) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } - } elseif (null === $object) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } throw new RuntimeError($message, $lineno, $source); } - } - - if (!\is_object($object)) { - if ($isDefinedTest) { - return false; - } - - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; - } - if (null === $object) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); - } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); - } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + if ($object instanceof Template) { + throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); } - throw new RuntimeError($message, $lineno, $source); - } - - if ($object instanceof Template) { - throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); - } + // object property + if (/* Template::METHOD_CALL */ 'method' !== $type) { + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { + if ($isDefinedTest) { + return true; + } - // object property - if (/* Template::METHOD_CALL */ 'method' !== $type) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { - if ($isDefinedTest) { - return true; - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + return $object->$item; } - - return $object->$item; } - } - static $cache = []; - - $class = \get_class($object); - - // object method - // precedence: getXxx() > isXxx() > hasXxx() - if (!isset($cache[$class])) { - $methods = get_class_methods($object); - sort($methods); - $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); - $classCache = []; - foreach ($methods as $i => $method) { - $classCache[$method] = $method; - $classCache[$lcName = $lcMethods[$i]] = $method; - - if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { - $name = substr($method, 2); - $lcName = substr($lcName, 2); - } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - if (\in_array('is'.$lcName, $lcMethods)) { + static $cache = []; + + $class = \get_class($object); + + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + sort($methods); + $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; + + if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + if (\in_array('is'.$lcName, $lcMethods)) { + continue; + } + } else { continue; } - } else { - continue; - } - // skip get() and is() methods (in which case, $name is empty) - if ($name) { - if (!isset($classCache[$name])) { - $classCache[$name] = $method; - } + // skip get() and is() methods (in which case, $name is empty) + if ($name) { + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } - if (!isset($classCache[$lcName])) { - $classCache[$lcName] = $method; + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } } } + $cache[$class] = $classCache; } - $cache[$class] = $classCache; - } - $call = false; - if (isset($cache[$class][$item])) { - $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { - $method = $cache[$class][$lcItem]; - } elseif (isset($cache[$class]['__call'])) { - $method = $item; - $call = true; - } else { - if ($isDefinedTest) { - return false; + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + if ($isDefinedTest) { + return true; } - throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } - if ($isDefinedTest) { - return true; - } + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; + } + throw $e; + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + return $ret; } - // Some objects throw exceptions when they have __call, and the method we try - // to call is not supported. If ignoreStrictCheck is true, we should return null. - try { - $ret = $object->$method(...$arguments); - } catch (\BadMethodCallException $e) { - if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { - return; + /** + * Returns the values from a single column in the input array. + * + *
+     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+     *
+     *  {% set fruits = items|column('fruit') %}
+     *
+     *  {# fruits now contains ['apple', 'orange'] #}
+     * 
+ * + * @param array|Traversable $array An array + * @param mixed $name The column name + * @param mixed $index The column to use as the index/keys for the returned array + * + * @return array The array of values + * + * @internal + */ + public static function arrayColumn($array, $name, $index = null): array + { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); } - throw $e; - } - - return $ret; -} -/** - * Returns the values from a single column in the input array. - * - *
- *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- *  {% set fruits = items|column('fruit') %}
- *
- *  {# fruits now contains ['apple', 'orange'] #}
- * 
- * - * @param array|Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array - * - * @return array The array of values - */ -function twig_array_column($array, $name, $index = null): array -{ - if ($array instanceof Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + return array_column($array, $name, $index); } - return array_column($array, $name, $index); -} + /** + * @internal + */ + public static function arrayFilter(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + } -function twig_array_filter(Environment $env, $array, $arrow) -{ - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); - } + self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); - twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter'); + if (\is_array($array)) { + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + } - if (\is_array($array)) { - return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); } - // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator - return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); -} + /** + * @internal + */ + public static function arrayMap(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); -function twig_array_map(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter'); + $r = []; + foreach ($array as $k => $v) { + $r[$k] = $arrow($v, $k); + } - $r = []; - foreach ($array as $k => $v) { - $r[$k] = $arrow($v, $k); + return $r; } - return $r; -} + /** + * @internal + */ + public static function arrayReduce(Environment $env, $array, $arrow, $initial = null) + { + self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); -function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter'); + if (!\is_array($array) && !$array instanceof \Traversable) { + throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + } - if (!\is_array($array) && !$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); - } + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $arrow($accumulator, $value, $key); + } - $accumulator = $initial; - foreach ($array as $key => $value) { - $accumulator = $arrow($accumulator, $value, $key); + return $accumulator; } - return $accumulator; -} - -function twig_array_some(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has some', 'operator'); + /** + * @internal + */ + public static function arraySome(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has some', 'operator'); - foreach ($array as $k => $v) { - if ($arrow($v, $k)) { - return true; + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } } - } - return false; -} + return false; + } -function twig_array_every(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has every', 'operator'); + /** + * @internal + */ + public static function arrayEvery(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has every', 'operator'); - foreach ($array as $k => $v) { - if (!$arrow($v, $k)) { - return false; + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } } - } - return true; -} + return true; + } -function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) -{ - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + /** + * @internal + */ + public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type) + { + if (!$arrow instanceof \Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { + throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + } } } -} diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 44428380239..075c13590ee 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -70,7 +70,7 @@ public function compile(Compiler $compiler): void $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs); if ($needsArrayMergeSpread) { - $compiler->raw('twig_array_merge('); + $compiler->raw('CoreExtension::arrayMerge('); } $compiler->raw('['); $first = true; diff --git a/src/Node/Expression/Binary/EqualBinary.php b/src/Node/Expression/Binary/EqualBinary.php index 6b48549ef26..5f423196fee 100644 --- a/src/Node/Expression/Binary/EqualBinary.php +++ b/src/Node/Expression/Binary/EqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 === twig_compare(') + ->raw('(0 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/GreaterBinary.php b/src/Node/Expression/Binary/GreaterBinary.php index e1dd06780b7..f42de3f8645 100644 --- a/src/Node/Expression/Binary/GreaterBinary.php +++ b/src/Node/Expression/Binary/GreaterBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(1 === twig_compare(') + ->raw('(1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/GreaterEqualBinary.php b/src/Node/Expression/Binary/GreaterEqualBinary.php index df9bfcfbf9d..0c4f43fd94a 100644 --- a/src/Node/Expression/Binary/GreaterEqualBinary.php +++ b/src/Node/Expression/Binary/GreaterEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 <= twig_compare(') + ->raw('(0 <= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/HasEveryBinary.php b/src/Node/Expression/Binary/HasEveryBinary.php index adfabd44c7f..c57bb20e916 100644 --- a/src/Node/Expression/Binary/HasEveryBinary.php +++ b/src/Node/Expression/Binary/HasEveryBinary.php @@ -18,7 +18,7 @@ class HasEveryBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_every($this->env, ') + ->raw('CoreExtension::arrayEvery($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/HasSomeBinary.php b/src/Node/Expression/Binary/HasSomeBinary.php index 270da369275..12293f84cb4 100644 --- a/src/Node/Expression/Binary/HasSomeBinary.php +++ b/src/Node/Expression/Binary/HasSomeBinary.php @@ -18,7 +18,7 @@ class HasSomeBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_some($this->env, ') + ->raw('CoreExtension::arraySome($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/InBinary.php b/src/Node/Expression/Binary/InBinary.php index 6dbfa97f05c..68a98fe15b3 100644 --- a/src/Node/Expression/Binary/InBinary.php +++ b/src/Node/Expression/Binary/InBinary.php @@ -18,7 +18,7 @@ class InBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_in_filter(') + ->raw('CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/LessBinary.php b/src/Node/Expression/Binary/LessBinary.php index 598e629134b..fb3264a2d47 100644 --- a/src/Node/Expression/Binary/LessBinary.php +++ b/src/Node/Expression/Binary/LessBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(-1 === twig_compare(') + ->raw('(-1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/LessEqualBinary.php b/src/Node/Expression/Binary/LessEqualBinary.php index e3c4af58d4c..8f3653892d6 100644 --- a/src/Node/Expression/Binary/LessEqualBinary.php +++ b/src/Node/Expression/Binary/LessEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 >= twig_compare(') + ->raw('(0 >= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index a8bce6f4e75..4669044e01a 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -18,7 +18,7 @@ class MatchesBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_matches(') + ->raw('CoreExtension::matches(') ->subcompile($this->getNode('right')) ->raw(', ') ->subcompile($this->getNode('left')) diff --git a/src/Node/Expression/Binary/NotEqualBinary.php b/src/Node/Expression/Binary/NotEqualBinary.php index db47a289050..d137ef62738 100644 --- a/src/Node/Expression/Binary/NotEqualBinary.php +++ b/src/Node/Expression/Binary/NotEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 !== twig_compare(') + ->raw('(0 !== CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/NotInBinary.php b/src/Node/Expression/Binary/NotInBinary.php index fcba6cca1cb..80c8755d8aa 100644 --- a/src/Node/Expression/Binary/NotInBinary.php +++ b/src/Node/Expression/Binary/NotInBinary.php @@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('!twig_in_filter(') + ->raw('!CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index 71269775c3d..e89b3897872 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -12,6 +12,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Extension\CoreExtension; use Twig\Node\Node; class FunctionExpression extends CallExpression @@ -33,7 +34,7 @@ public function compile(Compiler $compiler) $this->setAttribute('arguments', $function->getArguments()); $callable = $function->getCallable(); if ('constant' === $name && $this->getAttribute('is_defined_test')) { - $callable = 'twig_constant_is_defined'; + $callable = [CoreExtension::class, 'constantIsDefined']; } $this->setAttribute('callable', $callable); $this->setAttribute('is_variadic', $function->isVariadic()); diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index e6a75ce9404..29a446b881b 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -57,7 +57,7 @@ public function compile(Compiler $compiler): void return; } - $compiler->raw('twig_get_attribute($this->env, $this->source, '); + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); if ($this->getAttribute('ignore_strict_check')) { $this->getNode('node')->setAttribute('ignore_strict_check', true); diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index d5ec0b6efcb..6fa1c3f9e08 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -39,7 +39,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('twig_call_macro($macros[') + ->raw('CoreExtension::callMacro($macros[') ->repr($this->getNode('node')->getAttribute('name')) ->raw('], ') ->repr($this->getAttribute('method')) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 04addfbfe58..78b361d8a4e 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -42,7 +42,7 @@ public function compile(Compiler $compiler): void $compiler ->addDebugInfo($this) ->write("\$context['_parent'] = \$context;\n") - ->write("\$context['_seq'] = twig_ensure_traversable(") + ->write("\$context['_seq'] = CoreExtension::ensureTraversable(") ->subcompile($this->getNode('seq')) ->raw(");\n") ; diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index d540d6b23bf..be36b26574f 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -93,12 +93,12 @@ protected function addTemplateArguments(Compiler $compiler) $compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]'); } elseif (false === $this->getAttribute('only')) { $compiler - ->raw('twig_array_merge($context, ') + ->raw('CoreExtension::arrayMerge($context, ') ->subcompile($this->getNode('variables')) ->raw(')') ; } else { - $compiler->raw('twig_to_array('); + $compiler->raw('CoreExtension::toArray('); $compiler->subcompile($this->getNode('variables')); $compiler->raw(')'); } diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 9b485eeaf03..dce335c63f5 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -143,6 +143,7 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Environment;\n") ->write("use Twig\Error\LoaderError;\n") ->write("use Twig\Error\RuntimeError;\n") + ->write("use Twig\Extension\CoreExtension;\n") ->write("use Twig\Extension\SandboxExtension;\n") ->write("use Twig\Markup;\n") ->write("use Twig\Sandbox\SecurityError;\n") diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 2ac9123d0d1..302b40389ba 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -52,7 +52,7 @@ public function compile(Compiler $compiler): void ->raw(", \$this->getSourceContext());\n") ->outdent() ->write("}\n") - ->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName)) + ->write(sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) ; if ($this->getAttribute('only')) { diff --git a/src/Resources/core.php b/src/Resources/core.php new file mode 100644 index 00000000000..f58a1fdf261 --- /dev/null +++ b/src/Resources/core.php @@ -0,0 +1,497 @@ +env, $this->source, '; + return 'CoreExtension::getAttribute($this->env, $this->source, '; } } diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 431517f6b21..e1971aeea93 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Loader\LoaderInterface; class CoreTest extends TestCase @@ -26,7 +27,7 @@ public function testRandomFunction(array $expectedInArray, $value1, $value2 = nu $env = new Environment($this->createMock(LoaderInterface::class)); for ($i = 0; $i < 100; ++$i) { - $this->assertTrue(\in_array(twig_random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type + $this->assertTrue(\in_array(CoreExtension::random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type } } @@ -84,24 +85,24 @@ public function testRandomFunctionWithoutParameter() $max = mt_getrandmax(); for ($i = 0; $i < 100; ++$i) { - $val = twig_random(new Environment($this->createMock(LoaderInterface::class))); + $val = CoreExtension::random(new Environment($this->createMock(LoaderInterface::class))); $this->assertTrue(\is_int($val) && $val >= 0 && $val <= $max); } } public function testRandomFunctionReturnsAsIs() { - $this->assertSame('', twig_random(new Environment($this->createMock(LoaderInterface::class)), '')); - $this->assertSame('', twig_random(new Environment($this->createMock(LoaderInterface::class), ['charset' => null]), '')); + $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), '')); + $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class), ['charset' => null]), '')); $instance = new \stdClass(); - $this->assertSame($instance, twig_random(new Environment($this->createMock(LoaderInterface::class)), $instance)); + $this->assertSame($instance, CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), $instance)); } public function testRandomFunctionOfEmptyArrayThrowsException() { $this->expectException(RuntimeError::class); - twig_random(new Environment($this->createMock(LoaderInterface::class)), []); + CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), []); } public function testRandomFunctionOnNonUTF8String() @@ -111,7 +112,7 @@ public function testRandomFunctionOnNonUTF8String() $text = iconv('UTF-8', 'ISO-8859-1', 'Äé'); for ($i = 0; $i < 30; ++$i) { - $rand = twig_random($twig, $text); + $rand = CoreExtension::random($twig, $text); $this->assertTrue(\in_array(iconv('ISO-8859-1', 'UTF-8', $rand), ['Ä', 'é'], true)); } } @@ -122,7 +123,7 @@ public function testReverseFilterOnNonUTF8String() $twig->setCharset('ISO-8859-1'); $input = iconv('UTF-8', 'ISO-8859-1', 'Äé'); - $output = iconv('ISO-8859-1', 'UTF-8', twig_reverse_filter($twig, $input)); + $output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverseFilter($twig, $input)); $this->assertEquals($output, 'éÄ'); } @@ -133,7 +134,7 @@ public function testReverseFilterOnNonUTF8String() public function testTwigFirst($expected, $input) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_first($twig, $input)); + $this->assertSame($expected, CoreExtension::first($twig, $input)); } public function provideTwigFirstCases() @@ -155,7 +156,7 @@ public function provideTwigFirstCases() public function testTwigLast($expected, $input) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_last($twig, $input)); + $this->assertSame($expected, CoreExtension::last($twig, $input)); } public function provideTwigLastCases() @@ -176,7 +177,7 @@ public function provideTwigLastCases() */ public function testArrayKeysFilter(array $expected, $input) { - $this->assertSame($expected, twig_get_array_keys_filter($input)); + $this->assertSame($expected, CoreExtension::getArrayKeysFilter($input)); } public function provideArrayKeyCases() @@ -199,7 +200,7 @@ public function provideArrayKeyCases() */ public function testInFilter($expected, $value, $compare) { - $this->assertSame($expected, twig_in_filter($value, $compare)); + $this->assertSame($expected, CoreExtension::inFilter($value, $compare)); } public function provideInFilterCases() @@ -228,7 +229,7 @@ public function provideInFilterCases() public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_slice($twig, $input, $start, $length, $preserveKeys)); + $this->assertSame($expected, CoreExtension::slice($twig, $input, $start, $length, $preserveKeys)); } public function provideSliceFilterCases() @@ -257,16 +258,16 @@ public function provideSliceFilterCases() */ public function testCompare($expected, $a, $b) { - $this->assertSame($expected, twig_compare($a, $b)); - $this->assertSame($expected, -twig_compare($b, $a)); + $this->assertSame($expected, CoreExtension::compare($a, $b)); + $this->assertSame($expected, -CoreExtension::compare($b, $a)); } public function testCompareNAN() { - $this->assertSame(1, twig_compare(\NAN, 'NAN')); - $this->assertSame(1, twig_compare('NAN', \NAN)); - $this->assertSame(1, twig_compare(\NAN, 'foo')); - $this->assertSame(1, twig_compare('foo', \NAN)); + $this->assertSame(1, CoreExtension::compare(\NAN, 'NAN')); + $this->assertSame(1, CoreExtension::compare('NAN', \NAN)); + $this->assertSame(1, CoreExtension::compare(\NAN, 'foo')); + $this->assertSame(1, CoreExtension::compare('foo', \NAN)); } public function provideCompareCases() diff --git a/tests/Extension/StringLoaderExtensionTest.php b/tests/Extension/StringLoaderExtensionTest.php index e4fa2e3ba8c..4c67e12c719 100644 --- a/tests/Extension/StringLoaderExtensionTest.php +++ b/tests/Extension/StringLoaderExtensionTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; +use Twig\Extension\CoreExtension; use Twig\Extension\StringLoaderExtension; class StringLoaderExtensionTest extends TestCase @@ -21,6 +22,6 @@ public function testIncludeWithTemplateStringAndNoSandbox() { $twig = new Environment($this->createMock('\Twig\Loader\LoaderInterface')); $twig->addExtension(new StringLoaderExtension()); - $this->assertSame('something', twig_include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); + $this->assertSame('something', CoreExtension::include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); } } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index b8cc48fab16..18820cac8d1 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -69,7 +69,7 @@ protected function foobar() $node = $this->createFilter($expr, 'upper'); $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); - $tests[] = [$node, 'twig_number_format_filter($this->env, twig_upper_filter($this->env, "foo"), 2, ".", ",")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::numberFormatFilter($this->env, Twig\Extension\CoreExtension::upperFilter($this->env, "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); @@ -77,25 +77,25 @@ protected function foobar() 'timezone' => new ConstantExpression('America/Chicago', 1), 'format' => new ConstantExpression('d/m/Y H:i:s P', 1), ]); - $tests[] = [$node, 'twig_date_format_filter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; // skip an optional argument $date = new ConstantExpression(0, 1); $node = $this->createFilter($date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), ]); - $tests[] = [$node, 'twig_date_format_filter($this->env, 0, null, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, null, "America/Chicago")']; // underscores vs camelCase for named arguments $string = new ConstantExpression('abc', 1); $node = $this->createFilter($string, 'reverse', [ 'preserve_keys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'twig_reverse_filter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; $node = $this->createFilter($string, 'reverse', [ 'preserveKeys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'twig_reverse_filter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; // filter as an anonymous function $node = $this->createFilter(new ConstantExpression('foo', 1), 'anonymous'); diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index 05c07c02923..7fde216497c 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -76,7 +76,7 @@ public function getTests() 'timezone' => new ConstantExpression('America/Chicago', 1), 'date' => new ConstantExpression(0, 1), ]); - $tests[] = [$node, 'twig_date_converter($this->env, 0, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateConverter($this->env, 0, "America/Chicago")']; // arbitrary named arguments $node = $this->createFunction('barbar'); diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index ce746481771..dbfac3269b2 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -57,7 +57,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('items')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('items')}); foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) { echo {$this->getVariableGetter('foo')}; } @@ -78,7 +78,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -120,7 +120,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -162,7 +162,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['_iterated'] = false; \$context['loop'] = [ 'parent' => \$context['_parent'], diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index 92681662da7..6d96373bfd3 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -64,14 +64,14 @@ public function getTests() $node = new IncludeNode($expr, $vars, false, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(twig_array_merge(\$context, ["foo" => true])); +\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::arrayMerge(\$context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(twig_to_array(["foo" => true])); +\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -85,7 +85,7 @@ public function getTests() // ignore missing template } if (\$__internal_%s) { - \$__internal_%s->display(twig_to_array(["foo" => true])); + \$__internal_%s->display(CoreExtension::toArray(["foo" => true])); } EOF , null, true]; diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 938fa2ea7a2..d6b378ad56d 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -63,6 +63,7 @@ public function getTests() use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -110,7 +111,7 @@ public function getTemplateName() */ public function getDebugInfo() { - return array ( 37 => 1,); + return array ( 38 => 1,); } public function getSourceContext() @@ -133,6 +134,7 @@ public function getSourceContext() use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -195,7 +197,7 @@ public function isTraitable() */ public function getDebugInfo() { - return array ( 43 => 1, 41 => 2, 34 => 1,); + return array ( 44 => 1, 42 => 2, 35 => 1,); } public function getSourceContext() @@ -223,6 +225,7 @@ public function getSourceContext() use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -284,7 +287,7 @@ public function isTraitable() */ public function getDebugInfo() { - return array ( 43 => 2, 41 => 4, 34 => 2,); + return array ( 44 => 2, 42 => 4, 35 => 2,); } public function getSourceContext() diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 9c2364c1d21..b14a0384a8f 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -14,6 +14,7 @@ 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\Loader\LoaderInterface; @@ -94,7 +95,7 @@ public function testGetAttributeWithSandbox($object, $item, $allowed) $template = new TemplateForTest($twig); try { - twig_get_attribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true); + CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true); if (!$allowed) { $this->fail(); @@ -180,14 +181,14 @@ public function testGetAttributeOnArrayWithConfusableKey() $this->assertSame('IntegerButStringWithLeadingZeros', $array['01']); $this->assertSame('EmptyString', $array[null]); - $this->assertSame('Zero', twig_get_attribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('MinusOne', twig_get_attribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('FloatButString', twig_get_attribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('IntegerButStringWithLeadingZeros', twig_get_attribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('EmptyString', twig_get_attribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)'); + $this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)'); + $this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)'); + $this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)'); + $this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)'); + $this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)'); } /** @@ -198,7 +199,7 @@ public function testGetAttribute($defined, $value, $object, $item, $arguments, $ $twig = new Environment($this->createMock(LoaderInterface::class)); $template = new TemplateForTest($twig); - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } /** @@ -210,13 +211,13 @@ public function testGetAttributeStrict($defined, $value, $object, $item, $argume $template = new TemplateForTest($twig); if ($defined) { - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } else { $this->expectException(RuntimeError::class); if (null !== $exceptionMessage) { $this->expectExceptionMessage($exceptionMessage); } - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } } @@ -228,7 +229,7 @@ public function testGetAttributeDefined($defined, $value, $object, $item, $argum $twig = new Environment($this->createMock(LoaderInterface::class)); $template = new TemplateForTest($twig); - $this->assertEquals($defined, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); + $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } /** @@ -239,7 +240,7 @@ public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $twig = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); $template = new TemplateForTest($twig); - $this->assertEquals($defined, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); + $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } public function testGetAttributeCallExceptions() @@ -249,7 +250,7 @@ public function testGetAttributeCallExceptions() $object = new TemplateMagicMethodExceptionObject(); - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $object, 'foo')); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, 'foo')); } public function getGetAttributeTests() @@ -395,9 +396,9 @@ public function testGetIsMethods() $getIsObject = new TemplateGetIsMethods(); $template = new TemplateForTest($twig, 'index.twig'); // first time should not create a cache for "get" - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $getIsObject, 'get')); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 'get')); // 0 should be in the method cache now, so this should fail - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $getIsObject, 0)); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 0)); } } From aa7c454e2dccabb01343ff35cfadd5536b4b12d6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 20:24:52 +0100 Subject: [PATCH 10/10] Fix CS --- .../cssinliner-extra/CssInlinerExtension.php | 2 +- .../cssinliner-extra/Resources/functions.php | 1 + .../Tests/LegacyFunctionsTest.php | 1 + extra/html-extra/Resources/functions.php | 1 + extra/inky-extra/Resources/functions.php | 1 + .../inky-extra/Tests/LegacyFunctionsTest.php | 1 + extra/markdown-extra/MarkdownExtension.php | 8 ++++---- extra/markdown-extra/Resources/functions.php | 1 + .../Tests/LegacyFunctionsTest.php | 4 +++- src/Extension/CoreExtension.php | 3 +-- src/Extension/DebugExtension.php | 10 +++++----- src/Extension/EscaperExtension.php | 19 +++++++++---------- src/Extension/StringLoaderExtension.php | 2 +- 13 files changed, 30 insertions(+), 24 deletions(-) diff --git a/extra/cssinliner-extra/CssInlinerExtension.php b/extra/cssinliner-extra/CssInlinerExtension.php index 679e9165716..2ceb2e08598 100644 --- a/extra/cssinliner-extra/CssInlinerExtension.php +++ b/extra/cssinliner-extra/CssInlinerExtension.php @@ -33,7 +33,7 @@ public static function inlineCss(string $body, string ...$css): string if (null === $inliner) { $inliner = new CssToInlineStyles(); } - + return $inliner->convert($body, implode("\n", $css)); } } diff --git a/extra/cssinliner-extra/Resources/functions.php b/extra/cssinliner-extra/Resources/functions.php index c4b32db2d95..60305e231c5 100644 --- a/extra/cssinliner-extra/Resources/functions.php +++ b/extra/cssinliner-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function twig_inline_css(string $body, string ...$css): string diff --git a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php index 20feef172c9..d62e267fdff 100644 --- a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php +++ b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Extra\CssInliner\CssInlinerExtension; + use function Twig\Extra\CssInliner\twig_inline_css; /** diff --git a/extra/html-extra/Resources/functions.php b/extra/html-extra/Resources/functions.php index 4458f3bf8b7..ca18af1d344 100644 --- a/extra/html-extra/Resources/functions.php +++ b/extra/html-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function twig_html_classes(...$args): string diff --git a/extra/inky-extra/Resources/functions.php b/extra/inky-extra/Resources/functions.php index feef69eabe8..0fa4111debb 100644 --- a/extra/inky-extra/Resources/functions.php +++ b/extra/inky-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function twig_inky(string $body): string diff --git a/extra/inky-extra/Tests/LegacyFunctionsTest.php b/extra/inky-extra/Tests/LegacyFunctionsTest.php index 26ed1e76fe1..4810235b53f 100644 --- a/extra/inky-extra/Tests/LegacyFunctionsTest.php +++ b/extra/inky-extra/Tests/LegacyFunctionsTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Extra\Inky\InkyExtension; + use function Twig\Extra\Inky\twig_inky; /** diff --git a/extra/markdown-extra/MarkdownExtension.php b/extra/markdown-extra/MarkdownExtension.php index 1647b66ff70..6a245009556 100644 --- a/extra/markdown-extra/MarkdownExtension.php +++ b/extra/markdown-extra/MarkdownExtension.php @@ -31,21 +31,21 @@ public function getFilters() public static function htmlToMarkdown(string $body, array $options = []): string { static $converters; - + if (!class_exists(HtmlConverter::class)) { throw new \LogicException('You cannot use the "html_to_markdown" filter as league/html-to-markdown is not installed; try running "composer require league/html-to-markdown".'); } - + $options += [ 'hard_break' => true, 'strip_tags' => true, 'remove_nodes' => 'head style', ]; - + if (!isset($converters[$key = serialize($options)])) { $converters[$key] = new HtmlConverter($options); } - + return $converters[$key]->convert($body); } } diff --git a/extra/markdown-extra/Resources/functions.php b/extra/markdown-extra/Resources/functions.php index ad8da5a53b5..cf498364df0 100644 --- a/extra/markdown-extra/Resources/functions.php +++ b/extra/markdown-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function html_to_markdown(string $body, array $options = []): string diff --git a/extra/markdown-extra/Tests/LegacyFunctionsTest.php b/extra/markdown-extra/Tests/LegacyFunctionsTest.php index fe0ecdaa800..19a861de3ef 100644 --- a/extra/markdown-extra/Tests/LegacyFunctionsTest.php +++ b/extra/markdown-extra/Tests/LegacyFunctionsTest.php @@ -12,9 +12,11 @@ namespace Twig\Extra\Markdown\Tests; use PHPUnit\Framework\TestCase; -use Twig\Extra\Markdown\MarkdownExtension; + use function Twig\Extra\Markdown\html_to_markdown; +use Twig\Extra\Markdown\MarkdownExtension; + /** * @group legacy */ diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 8c9d0d4feff..ec90edbeab7 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -653,7 +653,6 @@ public static function arrayMerge(...$arrays) return $result; } - /** * Slices a variable. * @@ -1215,7 +1214,7 @@ public static function lowerFilter(Environment $env, $string) /** * Strips HTML and PHP tags from a string. * - * @param string|null $string + * @param string|null $string * @param string[]|string|null $string * * @return string diff --git a/src/Extension/DebugExtension.php b/src/Extension/DebugExtension.php index e023c7087b3..cefb44c5b8d 100644 --- a/src/Extension/DebugExtension.php +++ b/src/Extension/DebugExtension.php @@ -12,9 +12,9 @@ namespace Twig\Extension; use Twig\Environment; -use Twig\TwigFunction; use Twig\Template; use Twig\TemplateWrapper; +use Twig\TwigFunction; final class DebugExtension extends AbstractExtension { @@ -43,9 +43,9 @@ public static function dump(Environment $env, $context, ...$vars) if (!$env->isDebug()) { return; } - + ob_start(); - + if (!$vars) { $vars = []; foreach ($context as $key => $value) { @@ -53,12 +53,12 @@ public static function dump(Environment $env, $context, ...$vars) $vars[$key] = $value; } } - + var_dump($vars); } else { var_dump(...$vars); } - + return ob_get_clean(); } } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 88faa60a090..9a8c66c9047 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -21,7 +21,6 @@ use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TwigFilter; - final class EscaperExtension extends AbstractExtension { private $defaultStrategy; @@ -177,7 +176,7 @@ public static function escapeFilterIsSafe(Node $filterArgs) * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) * * @return string - * + * * @internal */ public static function escape(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) @@ -190,7 +189,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch if (\is_object($string) && method_exists($string, '__toString')) { if ($autoescape) { $c = \get_class($string); - $ext = $env->getExtension(EscaperExtension::class); + $ext = $env->getExtension(self::class); if (!isset($ext->safeClasses[$c])) { $ext->safeClasses[$c] = []; foreach (class_parents($string) + class_implements($string) as $class) { @@ -256,7 +255,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); } - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); return iconv('UTF-8', $charset, $string); @@ -265,7 +264,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch // escape all non-alphanumeric characters // into their \x or \uHHHH representations if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); } if (!preg_match('//u', $string)) { @@ -316,7 +315,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch case 'css': if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); } if (!preg_match('//u', $string)) { @@ -337,7 +336,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch case 'html_attr': if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); } if (!preg_match('//u', $string)) { @@ -404,14 +403,14 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch return rawurlencode($string); default: - $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); + $escapers = $env->getExtension(self::class)->getEscapers(); if (\array_key_exists($strategy, $escapers)) { return $escapers[$strategy]($env, $string, $charset); } - $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); + $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); + throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); } } } diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php index fde395dd810..9b25d9a554c 100644 --- a/src/Extension/StringLoaderExtension.php +++ b/src/Extension/StringLoaderExtension.php @@ -31,7 +31,7 @@ public function getFunctions(): array * * @param string $template A template as a string or object implementing __toString() * @param string $name An optional name of the template to be used in error messages - * + * * @internal */ public static function templateFromString(Environment $env, $template, string $name = null): TemplateWrapper