From 5462817da0d6065f087171e6b9caed5eaf236d61 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 16 May 2026 09:29:23 +0100 Subject: [PATCH] Add a `needs_is_sandboxed` option for filters, functions, and tests --- CHANGELOG | 1 + doc/advanced.rst | 28 +++ src/AbstractTwigCallable.php | 8 +- src/Attribute/AsTwigFilter.php | 2 + src/Attribute/AsTwigFunction.php | 2 + src/Attribute/AsTwigTest.php | 2 + src/Extension/AttributeExtension.php | 3 + src/Node/Expression/CallExpression.php | 30 ++++ src/TwigCallableInterface.php | 2 + src/Util/CallableArgumentsExtractor.php | 4 + tests/Extension/AttributeExtensionTest.php | 3 + .../Fixtures/ExtensionWithAttributes.php | 15 ++ tests/Extension/SandboxTest.php | 170 ++++++++++++++++++ tests/Node/Expression/FilterTest.php | 18 ++ 14 files changed, 287 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 03a5af5fb..4a255b8a8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.25.0 (2026-XX-XX) + * Add a `needs_is_sandboxed` option for filters, functions, and tests * Use deterministic suffixes for generated embed classes # 3.24.0 (2026-03-17) diff --git a/doc/advanced.rst b/doc/advanced.rst index 1a030e035..bda2fbc8d 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -216,6 +216,34 @@ the first argument to the filter call (or the second one if // ... }, ['needs_context' => true, 'needs_environment' => true]); +Sandbox-aware Filters +~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.25 + + The ``needs_is_sandboxed`` option was added in Twig 3.25. + +If you want to know whether the current template is sandboxed in your +filter, set the ``needs_is_sandboxed`` option to ``true``; Twig will pass the +current sandbox state as a boolean to the filter call (as the first +argument, or after the charset, the environment, and the context if they +are also requested):: + + $filter = new \Twig\TwigFilter('rot13', function (bool $isSandboxed, $string) { + if ($isSandboxed) { + // adjust behavior when running in a sandboxed template + } + + return str_rot13($string); + }, ['needs_is_sandboxed' => true]); + +The sandbox state is resolved against the current template source, which +means it takes both the global sandbox state and any ``SourcePolicy`` into +account. + +The same ``needs_is_sandboxed`` option is also available on functions and +tests. + Automatic Escaping ~~~~~~~~~~~~~~~~~~ diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index 804f336cb..008d4d395 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -32,6 +32,7 @@ abstract class AbstractTwigCallable implements TwigCallableInterface 'needs_environment' => false, 'needs_context' => false, 'needs_charset' => false, + 'needs_is_sandboxed' => false, 'is_variadic' => false, 'deprecation_info' => null, 'deprecated' => false, @@ -107,6 +108,11 @@ abstract class AbstractTwigCallable implements TwigCallableInterface return $this->options['needs_context']; } + public function needsIsSandboxed(): bool + { + return $this->options['needs_is_sandboxed']; + } + /** * @return static */ @@ -182,6 +188,6 @@ abstract class AbstractTwigCallable implements TwigCallableInterface public function getMinimalNumberOfRequiredArguments(): int { - return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + \count($this->arguments); + return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + ($this->options['needs_is_sandboxed'] ? 1 : 0) + \count($this->arguments); } } diff --git a/src/Attribute/AsTwigFilter.php b/src/Attribute/AsTwigFilter.php index 09f8f01de..59c4726de 100644 --- a/src/Attribute/AsTwigFilter.php +++ b/src/Attribute/AsTwigFilter.php @@ -35,6 +35,7 @@ final class AsTwigFilter * @param bool|null $needsCharset Whether the filter needs the charset passed as the first argument * @param bool|null $needsEnvironment Whether the filter needs the environment passed as the first argument, or after the charset * @param bool|null $needsContext Whether the filter needs the context array passed as the first argument, or after the charset and the environment + * @param bool|null $needsIsSandboxed Whether the filter needs the current sandbox state (a boolean) passed as the first argument, or after the charset, the environment, and the context * @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped * @param string|array|null $isSafeCallback Function called at compilation time to determine if the filter is safe * @param string|null $preEscape Some filters may need to work on input that is already escaped or safe @@ -46,6 +47,7 @@ final class AsTwigFilter public ?bool $needsCharset = null, public ?bool $needsEnvironment = null, public ?bool $needsContext = null, + public ?bool $needsIsSandboxed = null, public ?array $isSafe = null, public string|array|null $isSafeCallback = null, public ?string $preEscape = null, diff --git a/src/Attribute/AsTwigFunction.php b/src/Attribute/AsTwigFunction.php index d161f06f4..119918085 100644 --- a/src/Attribute/AsTwigFunction.php +++ b/src/Attribute/AsTwigFunction.php @@ -35,6 +35,7 @@ final class AsTwigFunction * @param bool|null $needsCharset Whether the function needs the charset passed as the first argument * @param bool|null $needsEnvironment Whether the function needs the environment passed as the first argument, or after the charset * @param bool|null $needsContext Whether the function needs the context array passed as the first argument, or after the charset and the environment + * @param bool|null $needsIsSandboxed Whether the function needs the current sandbox state (a boolean) passed as the first argument, or after the charset, the environment, and the context * @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped * @param string|array|null $isSafeCallback Function called at compilation time to determine if the function is safe * @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation @@ -44,6 +45,7 @@ final class AsTwigFunction public ?bool $needsCharset = null, public ?bool $needsEnvironment = null, public ?bool $needsContext = null, + public ?bool $needsIsSandboxed = null, public ?array $isSafe = null, public string|array|null $isSafeCallback = null, public ?DeprecatedCallableInfo $deprecationInfo = null, diff --git a/src/Attribute/AsTwigTest.php b/src/Attribute/AsTwigTest.php index 1ce2658b4..a5f32529b 100644 --- a/src/Attribute/AsTwigTest.php +++ b/src/Attribute/AsTwigTest.php @@ -35,6 +35,7 @@ final class AsTwigTest * @param bool|null $needsCharset Whether the test needs the charset passed as the first argument * @param bool|null $needsEnvironment Whether the test needs the environment passed as the first argument, or after the charset * @param bool|null $needsContext Whether the test needs the context array passed as the first argument, or after the charset and the environment + * @param bool|null $needsIsSandboxed Whether the test needs the current sandbox state (a boolean) passed as the first argument, or after the charset, the environment, and the context * @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation */ public function __construct( @@ -42,6 +43,7 @@ final class AsTwigTest public ?bool $needsCharset = null, public ?bool $needsEnvironment = null, public ?bool $needsContext = null, + public ?bool $needsIsSandboxed = null, public ?DeprecatedCallableInfo $deprecationInfo = null, ) { } diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index 74fcbb857..3b51f8dfc 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -95,6 +95,7 @@ final class AttributeExtension extends AbstractExtension 'needs_context' => $attribute->needsContext ?? false, 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), 'needs_charset' => $attribute->needsCharset ?? false, + 'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false, 'is_variadic' => $method->isVariadic(), 'is_safe' => $attribute->isSafe, 'is_safe_callback' => $attribute->isSafeCallback, @@ -118,6 +119,7 @@ final class AttributeExtension extends AbstractExtension 'needs_context' => $attribute->needsContext ?? false, 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), 'needs_charset' => $attribute->needsCharset ?? false, + 'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false, 'is_variadic' => $method->isVariadic(), 'is_safe' => $attribute->isSafe, 'is_safe_callback' => $attribute->isSafeCallback, @@ -139,6 +141,7 @@ final class AttributeExtension extends AbstractExtension 'needs_context' => $attribute->needsContext ?? false, 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), 'needs_charset' => $attribute->needsCharset ?? false, + 'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false, 'is_variadic' => $method->isVariadic(), 'deprecation_info' => $attribute->deprecationInfo, ]); diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index c97408f6d..70c56c3c6 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -100,6 +100,14 @@ abstract class CallExpression extends AbstractExpression $first = false; } + if (self::needsIsSandboxed($twigCallable)) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->raw('$this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source)'); + $first = false; + } + foreach ($twigCallable->getArguments() as $argument) { if (!$first) { $compiler->raw(', '); @@ -290,6 +298,9 @@ abstract class CallExpression extends AbstractExpression if ($twigCallable->needsContext()) { array_shift($parameters); } + if (self::needsIsSandboxed($twigCallable)) { + array_shift($parameters); + } foreach ($twigCallable->getArguments() as $argument) { array_shift($parameters); } @@ -320,6 +331,22 @@ abstract class CallExpression extends AbstractExpression return $this->reflector; } + /** + * @internal + * + * To be removed in 4.0 and replaced by $twigCallable->needsIsSandboxed(). + */ + public static function needsIsSandboxed(TwigCallableInterface $twigCallable): bool + { + if (method_exists($twigCallable, 'needsIsSandboxed')) { + return $twigCallable->needsIsSandboxed(); + } + + trigger_deprecation('twig/twig', '3.25', 'Not implementing the "needsIsSandboxed()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $twigCallable::class, TwigCallableInterface::class); + + return false; + } + /** * Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node). * @@ -334,6 +361,7 @@ abstract class CallExpression extends AbstractExpression $this->getAttribute('name'), $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), [ + 'needs_is_sandboxed' => $this->hasAttribute('needs_is_sandboxed') ? $this->getAttribute('needs_is_sandboxed') : self::needsIsSandboxed($current), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), @@ -344,6 +372,7 @@ abstract class CallExpression extends AbstractExpression 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'needs_is_sandboxed' => $this->hasAttribute('needs_is_sandboxed') ? $this->getAttribute('needs_is_sandboxed') : self::needsIsSandboxed($current), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), @@ -354,6 +383,7 @@ abstract class CallExpression extends AbstractExpression 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'needs_is_sandboxed' => $this->hasAttribute('needs_is_sandboxed') ? $this->getAttribute('needs_is_sandboxed') : self::needsIsSandboxed($current), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index 2a8ff6116..4279b6430 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -13,6 +13,8 @@ namespace Twig; /** * @author Fabien Potencier + * + * @method bool needsIsSandboxed() Whether the callable needs the current sandbox state passed as an argument. Not implementing this method is deprecated since Twig 3.25, it will be required in 4.0. */ interface TwigCallableInterface extends \Stringable { diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index d8625169d..c7bf54088 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -13,6 +13,7 @@ namespace Twig\Util; use Twig\Error\SyntaxError; use Twig\Node\Expression\ArrayExpression; +use Twig\Node\Expression\CallExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\VariadicExpression; use Twig\Node\Node; @@ -196,6 +197,9 @@ final class CallableArgumentsExtractor if ($this->twigCallable->needsContext()) { array_shift($parameters); } + if (CallExpression::needsIsSandboxed($this->twigCallable)) { + array_shift($parameters); + } foreach ($this->twigCallable->getArguments() as $argument) { array_shift($parameters); } diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php index 273ac2c24..41b215c30 100644 --- a/tests/Extension/AttributeExtensionTest.php +++ b/tests/Extension/AttributeExtensionTest.php @@ -48,6 +48,7 @@ class AttributeExtensionTest extends TestCase yield 'with env' => ['with_env_filter', 'withEnvFilter', ['needs_environment' => true]]; yield 'with context' => ['with_context_filter', 'withContextFilter', ['needs_context' => true]]; yield 'with env and context' => ['with_env_and_context_filter', 'withEnvAndContextFilter', ['needs_environment' => true, 'needs_context' => true]]; + yield 'with sandbox' => ['with_sandbox_filter', 'withSandboxFilter', ['needs_is_sandboxed' => true]]; yield 'variadic' => ['variadic_filter', 'variadicFilter', ['is_variadic' => true]]; yield 'deprecated' => ['deprecated_filter', 'deprecatedFilter', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]]; yield 'pattern' => ['pattern_*_filter', 'patternFilter', []]; @@ -76,6 +77,7 @@ class AttributeExtensionTest extends TestCase yield 'with env' => ['with_env_function', 'withEnvFunction', ['needs_environment' => true]]; yield 'with context' => ['with_context_function', 'withContextFunction', ['needs_context' => true]]; yield 'with env and context' => ['with_env_and_context_function', 'withEnvAndContextFunction', ['needs_environment' => true, 'needs_context' => true]]; + yield 'with sandbox' => ['with_sandbox_function', 'withSandboxFunction', ['needs_is_sandboxed' => true]]; yield 'no argument' => ['no_arg_function', 'noArgFunction', []]; yield 'variadic' => ['variadic_function', 'variadicFunction', ['is_variadic' => true]]; yield 'deprecated' => ['deprecated_function', 'deprecatedFunction', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]]; @@ -104,6 +106,7 @@ class AttributeExtensionTest extends TestCase yield 'with env' => ['with_env_test', 'withEnvTest', ['needs_environment' => true]]; yield 'with context' => ['with_context_test', 'withContextTest', ['needs_context' => true]]; yield 'with env and context' => ['with_env_and_context_test', 'withEnvAndContextTest', ['needs_environment' => true, 'needs_context' => true]]; + yield 'with sandbox' => ['with_sandbox_test', 'withSandboxTest', ['needs_is_sandboxed' => true]]; yield 'variadic' => ['variadic_test', 'variadicTest', ['is_variadic' => true]]; yield 'deprecated' => ['deprecated_test', 'deprecatedTest', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]]; } diff --git a/tests/Extension/Fixtures/ExtensionWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php index ed4dff31f..181029ce8 100644 --- a/tests/Extension/Fixtures/ExtensionWithAttributes.php +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -39,6 +39,11 @@ class ExtensionWithAttributes { } + #[AsTwigFilter('with_sandbox_filter', needsIsSandboxed: true)] + public function withSandboxFilter(bool $isSandboxed, string $string) + { + } + #[AsTwigFilter('variadic_filter')] public function variadicFilter(string ...$strings) { @@ -74,6 +79,11 @@ class ExtensionWithAttributes { } + #[AsTwigFunction('with_sandbox_function', needsIsSandboxed: true)] + public function withSandboxFunction(bool $isSandboxed, string $string) + { + } + #[AsTwigFunction('no_arg_function')] public function noArgFunction() { @@ -114,6 +124,11 @@ class ExtensionWithAttributes { } + #[AsTwigTest('with_sandbox_test', needsIsSandboxed: true)] + public function withSandboxTest(bool $isSandboxed, $argument) + { + } + #[AsTwigTest('deprecated_test', deprecationInfo: new DeprecatedCallableInfo('foo/bar', '1.2'))] public function deprecatedTest($value, $argument) { diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 6d8e5035a..2c5831317 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -606,6 +606,176 @@ EOF $this->expectException(SecurityError::class); $twig->load('1_basic')->render([]); } + + public function testNeedsIsSandboxedFilterReceivesTrueWhenSandboxed() + { + $twig = $this->getEnvironment(true, [], ['index' => '{{ "foo"|sandbox_aware }}'], [], ['sandbox_aware']); + $twig->addFilter(new \Twig\TwigFilter('sandbox_aware', static function (bool $isSandboxed, string $value) { + return $value.':'.($isSandboxed ? 'on' : 'off'); + }, ['needs_is_sandboxed' => true])); + + $this->assertSame('foo:on', $twig->load('index')->render([])); + } + + public function testNeedsIsSandboxedFilterReceivesFalseWhenNotSandboxed() + { + $twig = $this->getEnvironment(false, [], ['index' => '{{ "foo"|sandbox_aware }}']); + $twig->addFilter(new \Twig\TwigFilter('sandbox_aware', static function (bool $isSandboxed, string $value) { + return $value.':'.($isSandboxed ? 'on' : 'off'); + }, ['needs_is_sandboxed' => true])); + + $this->assertSame('foo:off', $twig->load('index')->render([])); + } + + public function testNeedsIsSandboxedFilterFollowsSourcePolicy() + { + $twig = $this->getEnvironment(false, [], [ + 'in' => '{{ "foo"|sandbox_aware }}', + 'out' => '{{ "foo"|sandbox_aware }}', + ], [], ['sandbox_aware'], [], [], [], new class implements \Twig\Sandbox\SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return 'in' === $source->getName(); + } + }); + $twig->addFilter(new \Twig\TwigFilter('sandbox_aware', static function (bool $isSandboxed, string $value) { + return $value.':'.($isSandboxed ? 'on' : 'off'); + }, ['needs_is_sandboxed' => true])); + + $this->assertSame('foo:on', $twig->load('in')->render([])); + $this->assertSame('foo:off', $twig->load('out')->render([])); + } + + public function testNeedsIsSandboxedFunctionWithoutSandboxExtension() + { + $loader = new ArrayLoader(['index' => '{{ sandbox_aware("foo") }}']); + $twig = new Environment($loader, ['debug' => true, 'cache' => false, 'autoescape' => false]); + $twig->addFunction(new \Twig\TwigFunction('sandbox_aware', static function (bool $isSandboxed, string $value) { + return $value.':'.($isSandboxed ? 'on' : 'off'); + }, ['needs_is_sandboxed' => true])); + + $this->assertSame('foo:off', $twig->load('index')->render([])); + } + + public function testNeedsIsSandboxedTestReceivesTrueWhenSandboxed() + { + $twig = $this->getEnvironment(true, [], ['index' => '{{ "foo" is sandbox_aware ? "on" : "off" }}']); + $twig->addTest(new \Twig\TwigTest('sandbox_aware', static function (bool $isSandboxed, string $value) { + return $isSandboxed && 'foo' === $value; + }, ['needs_is_sandboxed' => true])); + + $this->assertSame('on', $twig->load('index')->render([])); + } + + public function testNeedsIsSandboxedTestReceivesFalseWhenNotSandboxed() + { + $twig = $this->getEnvironment(false, [], ['index' => '{{ "foo" is sandbox_aware ? "on" : "off" }}']); + $twig->addTest(new \Twig\TwigTest('sandbox_aware', static function (bool $isSandboxed, string $value) { + return !$isSandboxed && 'foo' === $value; + }, ['needs_is_sandboxed' => true])); + + $this->assertSame('on', $twig->load('index')->render([])); + } + + /** + * @group legacy + */ + public function testNeedsIsSandboxedHelperTriggersDeprecationForCustomImplementation() + { + $callable = new LegacyTwigCallableWithoutNeedsIsSandboxed(); + + $this->expectDeprecation(\sprintf('Since twig/twig 3.25: Not implementing the "needsIsSandboxed()" method in "%s" is deprecated. This method will be part of the "Twig\TwigCallableInterface" interface in 4.0.', $callable::class)); + + $this->assertFalse(\Twig\Node\Expression\CallExpression::needsIsSandboxed($callable)); + } +} + +class LegacyTwigCallableWithoutNeedsIsSandboxed implements \Twig\TwigCallableInterface +{ + public function getName(): string + { + return 'foo'; + } + + public function getType(): string + { + return 'filter'; + } + + public function getDynamicName(): string + { + return 'foo'; + } + + public function getCallable() + { + return null; + } + + public function getNodeClass(): string + { + return ''; + } + + public function needsCharset(): bool + { + return false; + } + + public function needsEnvironment(): bool + { + return false; + } + + public function needsContext(): bool + { + return false; + } + + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): \Twig\TwigCallableInterface + { + return $this; + } + + public function getArguments(): array + { + return []; + } + + public function isVariadic(): bool + { + return false; + } + + public function isDeprecated(): bool + { + return false; + } + + public function getDeprecatingPackage(): string + { + return ''; + } + + public function getDeprecatedVersion(): string + { + return ''; + } + + public function getAlternative(): ?string + { + return null; + } + + public function getMinimalNumberOfRequiredArguments(): int + { + return 0; + } + + public function __toString(): string + { + return 'foo'; + } } class ParentClass diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 5673002b5..e77dd5bb8 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -87,6 +87,14 @@ class FilterTest extends NodeTestCase $node = self::createFilter($environment, new ConstantExpression('foo', 1), 'anonymous'); $tests[] = [$node, '$this->env->getFilter(\'anonymous\')->getCallable()("foo")']; + // needs sandbox + $node = self::createFilter($environment, $string, 'bar_sandbox'); + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_sandbox($this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source), "abc")', $environment]; + + // needs charset, environment, context, and sandbox + $node = self::createFilter($environment, $string, 'bar_all'); + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_all($this->env->getCharset(), $this->env, $context, $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source), "abc")', $environment]; + // needs environment $node = self::createFilter($environment, $string, 'bar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_dummy($this->env, "abc")', $environment]; @@ -178,6 +186,8 @@ class FilterTest extends NodeTestCase $env->addFilter(new TwigFilter('bar', 'Twig\Tests\Node\Expression\twig_tests_filter_dummy', ['needs_environment' => true])); $env->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); $env->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); + $env->addFilter(new TwigFilter('bar_sandbox', 'Twig\Tests\Node\Expression\twig_tests_filter_sandbox', ['needs_is_sandboxed' => true])); + $env->addFilter(new TwigFilter('bar_all', 'Twig\Tests\Node\Expression\twig_tests_filter_all', ['needs_charset' => true, 'needs_environment' => true, 'needs_context' => true, 'needs_is_sandboxed' => true])); $env->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall')); if (\PHP_VERSION_ID >= 80111) { $env->addExtension(new FilterTestExtension()); @@ -217,6 +227,14 @@ function twig_tests_filter_barbar($context, $string, $arg1 = null, $arg2 = null, { } +function twig_tests_filter_sandbox(bool $isSandboxed, $string) +{ +} + +function twig_tests_filter_all(string $charset, Environment $env, array $context, bool $isSandboxed, $string) +{ +} + class ChildMagicCallStub extends ParentMagicCallStub { public static function identifier()