mirror of
https://github.com/Combodo/iTop.git
synced 2026-08-18 19:58:28 +02:00
N°9635 - Twig security issues CVE-2026-46633 and CVE-2026-46639 and CVE-2026-46640
This commit is contained in:
15
composer.lock
generated
15
composer.lock
generated
@@ -4787,16 +4787,16 @@
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.23.0",
|
||||
"version": "v3.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
|
||||
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
|
||||
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4806,7 +4806,8 @@
|
||||
"symfony/polyfill-mbstring": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"php-cs-fixer/shim": "^3.0@stable",
|
||||
"phpstan/phpstan": "^2.0@stable",
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
|
||||
},
|
||||
@@ -4850,7 +4851,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.23.0"
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4862,7 +4863,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-23T21:00:41+00:00"
|
||||
"time": "2026-07-03T20:44:34+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
||||
@@ -2917,6 +2917,7 @@ return array(
|
||||
'Twig\\Markup' => $vendorDir . '/twig/twig/src/Markup.php',
|
||||
'Twig\\NodeTraverser' => $vendorDir . '/twig/twig/src/NodeTraverser.php',
|
||||
'Twig\\NodeVisitor\\AbstractNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php',
|
||||
'Twig\\NodeVisitor\\CorrectnessNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/CorrectnessNodeVisitor.php',
|
||||
'Twig\\NodeVisitor\\EscaperNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php',
|
||||
'Twig\\NodeVisitor\\NodeVisitorInterface' => $vendorDir . '/twig/twig/src/NodeVisitor/NodeVisitorInterface.php',
|
||||
'Twig\\NodeVisitor\\OptimizerNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php',
|
||||
@@ -2931,6 +2932,8 @@ return array(
|
||||
'Twig\\Node\\CheckSecurityCallNode' => $vendorDir . '/twig/twig/src/Node/CheckSecurityCallNode.php',
|
||||
'Twig\\Node\\CheckSecurityNode' => $vendorDir . '/twig/twig/src/Node/CheckSecurityNode.php',
|
||||
'Twig\\Node\\CheckToStringNode' => $vendorDir . '/twig/twig/src/Node/CheckToStringNode.php',
|
||||
'Twig\\Node\\CoercesChildrenToStringInterface' => $vendorDir . '/twig/twig/src/Node/CoercesChildrenToStringInterface.php',
|
||||
'Twig\\Node\\ConfigNode' => $vendorDir . '/twig/twig/src/Node/ConfigNode.php',
|
||||
'Twig\\Node\\DeprecatedNode' => $vendorDir . '/twig/twig/src/Node/DeprecatedNode.php',
|
||||
'Twig\\Node\\DoNode' => $vendorDir . '/twig/twig/src/Node/DoNode.php',
|
||||
'Twig\\Node\\EmbedNode' => $vendorDir . '/twig/twig/src/Node/EmbedNode.php',
|
||||
@@ -3069,12 +3072,15 @@ return array(
|
||||
'Twig\\Sandbox\\SecurityNotAllowedMethodError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedMethodError.php',
|
||||
'Twig\\Sandbox\\SecurityNotAllowedPropertyError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedPropertyError.php',
|
||||
'Twig\\Sandbox\\SecurityNotAllowedTagError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php',
|
||||
'Twig\\Sandbox\\SecurityNotAllowedTestError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedTestError.php',
|
||||
'Twig\\Sandbox\\SecurityPolicy' => $vendorDir . '/twig/twig/src/Sandbox/SecurityPolicy.php',
|
||||
'Twig\\Sandbox\\SecurityPolicyInterface' => $vendorDir . '/twig/twig/src/Sandbox/SecurityPolicyInterface.php',
|
||||
'Twig\\Sandbox\\SourcePolicyInterface' => $vendorDir . '/twig/twig/src/Sandbox/SourcePolicyInterface.php',
|
||||
'Twig\\Source' => $vendorDir . '/twig/twig/src/Source.php',
|
||||
'Twig\\Template' => $vendorDir . '/twig/twig/src/Template.php',
|
||||
'Twig\\TemplateWrapper' => $vendorDir . '/twig/twig/src/TemplateWrapper.php',
|
||||
'Twig\\Test\\IntegrationTestCase' => $vendorDir . '/twig/twig/src/Test/IntegrationTestCase.php',
|
||||
'Twig\\Test\\NodeTestCase' => $vendorDir . '/twig/twig/src/Test/NodeTestCase.php',
|
||||
'Twig\\Token' => $vendorDir . '/twig/twig/src/Token.php',
|
||||
'Twig\\TokenParser\\AbstractTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/AbstractTokenParser.php',
|
||||
'Twig\\TokenParser\\ApplyTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/ApplyTokenParser.php',
|
||||
@@ -3104,6 +3110,7 @@ return array(
|
||||
'Twig\\TwigFunction' => $vendorDir . '/twig/twig/src/TwigFunction.php',
|
||||
'Twig\\TwigTest' => $vendorDir . '/twig/twig/src/TwigTest.php',
|
||||
'Twig\\Util\\CallableArgumentsExtractor' => $vendorDir . '/twig/twig/src/Util/CallableArgumentsExtractor.php',
|
||||
'Twig\\Util\\CallableParameters' => $vendorDir . '/twig/twig/src/Util/CallableParameters.php',
|
||||
'Twig\\Util\\DeprecationCollector' => $vendorDir . '/twig/twig/src/Util/DeprecationCollector.php',
|
||||
'Twig\\Util\\ReflectionCallable' => $vendorDir . '/twig/twig/src/Util/ReflectionCallable.php',
|
||||
'Twig\\Util\\TemplateDirIterator' => $vendorDir . '/twig/twig/src/Util/TemplateDirIterator.php',
|
||||
|
||||
@@ -3272,6 +3272,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Twig\\Markup' => __DIR__ . '/..' . '/twig/twig/src/Markup.php',
|
||||
'Twig\\NodeTraverser' => __DIR__ . '/..' . '/twig/twig/src/NodeTraverser.php',
|
||||
'Twig\\NodeVisitor\\AbstractNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php',
|
||||
'Twig\\NodeVisitor\\CorrectnessNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/CorrectnessNodeVisitor.php',
|
||||
'Twig\\NodeVisitor\\EscaperNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php',
|
||||
'Twig\\NodeVisitor\\NodeVisitorInterface' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/NodeVisitorInterface.php',
|
||||
'Twig\\NodeVisitor\\OptimizerNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php',
|
||||
@@ -3286,6 +3287,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Twig\\Node\\CheckSecurityCallNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckSecurityCallNode.php',
|
||||
'Twig\\Node\\CheckSecurityNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckSecurityNode.php',
|
||||
'Twig\\Node\\CheckToStringNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckToStringNode.php',
|
||||
'Twig\\Node\\CoercesChildrenToStringInterface' => __DIR__ . '/..' . '/twig/twig/src/Node/CoercesChildrenToStringInterface.php',
|
||||
'Twig\\Node\\ConfigNode' => __DIR__ . '/..' . '/twig/twig/src/Node/ConfigNode.php',
|
||||
'Twig\\Node\\DeprecatedNode' => __DIR__ . '/..' . '/twig/twig/src/Node/DeprecatedNode.php',
|
||||
'Twig\\Node\\DoNode' => __DIR__ . '/..' . '/twig/twig/src/Node/DoNode.php',
|
||||
'Twig\\Node\\EmbedNode' => __DIR__ . '/..' . '/twig/twig/src/Node/EmbedNode.php',
|
||||
@@ -3424,12 +3427,15 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Twig\\Sandbox\\SecurityNotAllowedMethodError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedMethodError.php',
|
||||
'Twig\\Sandbox\\SecurityNotAllowedPropertyError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedPropertyError.php',
|
||||
'Twig\\Sandbox\\SecurityNotAllowedTagError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php',
|
||||
'Twig\\Sandbox\\SecurityNotAllowedTestError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedTestError.php',
|
||||
'Twig\\Sandbox\\SecurityPolicy' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityPolicy.php',
|
||||
'Twig\\Sandbox\\SecurityPolicyInterface' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityPolicyInterface.php',
|
||||
'Twig\\Sandbox\\SourcePolicyInterface' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SourcePolicyInterface.php',
|
||||
'Twig\\Source' => __DIR__ . '/..' . '/twig/twig/src/Source.php',
|
||||
'Twig\\Template' => __DIR__ . '/..' . '/twig/twig/src/Template.php',
|
||||
'Twig\\TemplateWrapper' => __DIR__ . '/..' . '/twig/twig/src/TemplateWrapper.php',
|
||||
'Twig\\Test\\IntegrationTestCase' => __DIR__ . '/..' . '/twig/twig/src/Test/IntegrationTestCase.php',
|
||||
'Twig\\Test\\NodeTestCase' => __DIR__ . '/..' . '/twig/twig/src/Test/NodeTestCase.php',
|
||||
'Twig\\Token' => __DIR__ . '/..' . '/twig/twig/src/Token.php',
|
||||
'Twig\\TokenParser\\AbstractTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/AbstractTokenParser.php',
|
||||
'Twig\\TokenParser\\ApplyTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/ApplyTokenParser.php',
|
||||
@@ -3459,6 +3465,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Twig\\TwigFunction' => __DIR__ . '/..' . '/twig/twig/src/TwigFunction.php',
|
||||
'Twig\\TwigTest' => __DIR__ . '/..' . '/twig/twig/src/TwigTest.php',
|
||||
'Twig\\Util\\CallableArgumentsExtractor' => __DIR__ . '/..' . '/twig/twig/src/Util/CallableArgumentsExtractor.php',
|
||||
'Twig\\Util\\CallableParameters' => __DIR__ . '/..' . '/twig/twig/src/Util/CallableParameters.php',
|
||||
'Twig\\Util\\DeprecationCollector' => __DIR__ . '/..' . '/twig/twig/src/Util/DeprecationCollector.php',
|
||||
'Twig\\Util\\ReflectionCallable' => __DIR__ . '/..' . '/twig/twig/src/Util/ReflectionCallable.php',
|
||||
'Twig\\Util\\TemplateDirIterator' => __DIR__ . '/..' . '/twig/twig/src/Util/TemplateDirIterator.php',
|
||||
|
||||
@@ -5206,17 +5206,17 @@
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.23.0",
|
||||
"version_normalized": "3.23.0.0",
|
||||
"version": "v3.28.0",
|
||||
"version_normalized": "3.28.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
|
||||
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
|
||||
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5226,11 +5226,12 @@
|
||||
"symfony/polyfill-mbstring": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"php-cs-fixer/shim": "^3.0@stable",
|
||||
"phpstan/phpstan": "^2.0@stable",
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
|
||||
},
|
||||
"time": "2026-01-23T21:00:41+00:00",
|
||||
"time": "2026-07-03T20:44:34+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
@@ -5272,7 +5273,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.23.0"
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'combodo/itop',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => '4b4fe55060b045962c6954ebff6ca624f87df3f5',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -22,9 +22,9 @@
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'combodo/itop' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'pretty_version' => 'dev-develop',
|
||||
'version' => 'dev-develop',
|
||||
'reference' => '4b4fe55060b045962c6954ebff6ca624f87df3f5',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -679,9 +679,9 @@
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'twig/twig' => array(
|
||||
'pretty_version' => 'v3.23.0',
|
||||
'version' => '3.23.0.0',
|
||||
'reference' => 'a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9',
|
||||
'pretty_version' => 'v3.28.0',
|
||||
'version' => '3.28.0.0',
|
||||
'reference' => '597c12ed286fb9d1701a36684ce6e0cbe28ebc8b',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../twig/twig',
|
||||
'aliases' => array(),
|
||||
|
||||
@@ -1,3 +1,85 @@
|
||||
# 3.28.0 (2026-07-03)
|
||||
|
||||
* Render backed enums using their backing value in the `html_attr` function
|
||||
* Fix empty Markup values being treated as truthy in and, or, xor, not, ternary, and elvis expressions
|
||||
* Fix a PHP 8.5 `chr()` deprecation when decoding an octal string escape sequence larger than `\377` (such as `"\777"`)
|
||||
* Mark `Twig\Markup` as `@final`; it will be final in Twig 4.0
|
||||
* Reduce memory usage and speed up the context restoration compiled at the end of `for` loops
|
||||
* Allow calling a macro with a dynamic name via the dot operator (`macros.(name)(args)`)
|
||||
* Report the column number in syntax errors and expose it via `Error::getTemplateColumn()`
|
||||
* Track the source offset of each token and expose it via `Token::getOffset()`
|
||||
* Fix nested `block()` calls to resolve against the overriding template when a block rendered through `block(name, template)` calls `parent()`
|
||||
* Deprecate the possibility to use a `block` tag within a capture node (like `set`)
|
||||
* Deprecate the possibility to use a `block` tag within a capture node (like `set`) in child templates
|
||||
* Deprecate using a `macro`, `extends`, or `use` tag outside the root of a template
|
||||
* Stop reporting a skipped test in `IntegrationTestCase` when there is no legacy test to run
|
||||
* Fix `markdown_to_html` to strip the indentation shared by all lines instead of mangling content that starts with a blank line
|
||||
* Make the `IntegrationTestCase` and `NodeTestCase` test helpers compatible with PHPUnit 11
|
||||
* Cast printed expressions to string so values that cannot be converted to a string (arrays, non-`Stringable` objects, ...) report a usable stack trace at the print location
|
||||
* Make the `include()` function return a `Markup` object so an assigned result is not re-escaped when printed
|
||||
* Skip the sandbox `__toString` check on arguments whose PHP parameter type cannot implicitly coerce to string
|
||||
* Document the criteria for marking a callable or tag as always allowed in a sandbox, and the list of built-in tags, filters, and functions that will be always allowed in Twig 4.0
|
||||
* Add an `always_allowed_in_sandbox` option for filters, functions, and tests, and an `isAlwaysAllowedInSandbox()` method for token parsers, to let authors mark callables and tags that are always allowed in sandbox mode without explicit allow-listing
|
||||
* Add an allow-list for tests to `Twig\Sandbox\SecurityPolicy`, with the safe built-in tests flagged as always allowed so they keep working without allow-listing
|
||||
|
||||
# 3.27.1 (2026-05-30)
|
||||
|
||||
* Fix array access with a `Stringable` key to coerce the key to string consistently instead of throwing in the optimized path
|
||||
* Fix sandbox replacing `IteratorAggregate` arguments (e.g. Symfony's `FormView`) by a plain array
|
||||
|
||||
# 3.27.0 (2026-05-27)
|
||||
|
||||
* Add a strict mode to `Twig\Sandbox\SecurityPolicy` to opt-in to the 4.0 behavior for the `extends`/`use` tags and the `parent`/`block`/`attribute` functions, which are otherwise still implicitly allowed in a sandbox
|
||||
* Deprecate the fact that the `parent`, `block`, and `attribute` functions are always allowed in a sandboxed template
|
||||
* Fix sandbox filter/tag/function allow-list bypass when the sandbox state changed between renders of a cached `Template` instance
|
||||
* Fix PHP 8.1+ implicit float-to-int deprecation triggered by sandboxed `ArrayAccess` attribute access with a float key
|
||||
* Restrict allowed classes in `Twig\Profiler\Profile::unserialize()` to prevent arbitrary class instantiation
|
||||
* Escape root profile name in `HtmlDumper`
|
||||
* Fix sandbox bypass in deprecated internal wrappers `twig_array_some()`, `twig_array_every()`, and `twig_check_arrow_in_sandbox()` (`src/Resources/core.php`)
|
||||
* Deprecate the `Twig\Sandbox\SourcePolicyInterface` interface with no replacement
|
||||
* Fix sandbox bypass in the "column" filter when sandboxing is enabled via `SourcePolicyInterface`
|
||||
* Fix sandbox `__toString` bypass via `Traversable` arguments to the `join` and `replace` filters (also covers containers that implement both `Stringable` and `Traversable`)
|
||||
* Fix sandbox `__toString` bypass via the `in` and `not in` operators
|
||||
* Prevent a stack overflow in `SandboxExtension::ensureToStringAllowed()` when a self-referencing iterable is passed to a sandboxed template
|
||||
* Add support for any expression as a dynamic mapping key (attribute access, filters, ...)
|
||||
* Fix sandbox `__toString` policy bypass via dynamic mapping keys
|
||||
|
||||
# 3.26.0 (2026-05-20)
|
||||
|
||||
* Document that the sandbox doesn't protect against resource exhaustion
|
||||
* Document `template_from_string` caveats when used in a sandboxed environment
|
||||
* Add docs on `Markup` about the goal of this class in the context of a sandbox
|
||||
* Pre-escape HTML input on the `spaceless` filter
|
||||
* Pre-escape HTML input on `inline_css` and `inky_to_html` filters
|
||||
* Fix XSS by adjusting `is_safe` annotation on HTML-emitting filters
|
||||
* [Profiler] Escape template and profile names in `HtmlDumper`
|
||||
* Fix unbounded memoisation of `IntlDateFormatter` / `NumberFormatter`
|
||||
* Fix sandbox bypass in the "column" filter
|
||||
* Fix sandbox bypass in the `{% sandbox %}` tag when including a preloaded template
|
||||
* Fix sandbox bypass: PHP code injection via `{% use %}` template name
|
||||
* Fix sandbox bypass: PHP code injection via `_self` / import macro reference
|
||||
* Fix sandbox bypass in object destructuring assignment
|
||||
* Fix sandbox bypass: propagate `Source` to `checkArrow` for source-policy sandboxing
|
||||
* Encode single quotes as `\x27` in `Compiler::string()` as a defense-in-depth measure
|
||||
* Fix sandbox `__toString` bypasses
|
||||
* Add `Twig\Node\CoercesChildrenToStringInterface` to let nodes declare which of their child nodes will be string-coerced at runtime so the sandbox wraps them with a `__toString` check
|
||||
|
||||
# 3.25.0 (2026-05-17)
|
||||
|
||||
* Add a `needs_is_sandboxed` option for filters, functions, and tests
|
||||
* Use deterministic suffixes for generated embed classes
|
||||
* Lazy-load `EscaperRuntime` in `EscaperExtension`
|
||||
|
||||
# 3.24.0 (2026-03-17)
|
||||
|
||||
* Deprecate not implementing the `getOperatorTokens()` method in `ExpressionParserInterface` implementations
|
||||
* Deprecate passing a non-`AbstractExpression` node to `Twig\Node\Expression\Binary\MatchesBinary` constructor
|
||||
* Deprecate passing a non-`AbstractExpression` node to `Parser::setParent()`
|
||||
* Add support for renaming variables in object destructuring (`{name: userName} = user`)
|
||||
* Add `html_attr_relaxed` escaping strategy that preserves :, @, [, and ] for front-end framework attribute names
|
||||
* Add support for short-circuiting in null-safe operator chains
|
||||
* Add the `html_attr` function and `html_attr_merge` as well as `html_attr_type` filters
|
||||
|
||||
# 3.23.0 (2026-01-23)
|
||||
|
||||
* Add `=` assignment operator (allows to set variables in expression or to replace the short-form of the set tag)
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"phpstan/phpstan": "^2.0"
|
||||
"phpstan/phpstan": "^2.0@stable",
|
||||
"php-cs-fixer/shim": "^3.0@stable"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"subtrees": {
|
||||
"twig-extra-bundle": "extra/twig-extra-bundle",
|
||||
"cache-extra": "extra/cache-extra",
|
||||
"cssinliner-extra": "extra/cssinliner-extra",
|
||||
"html-extra": "extra/html-extra",
|
||||
"inky-extra": "extra/inky-extra",
|
||||
"intl-extra": "extra/intl-extra",
|
||||
"markdown-extra": "extra/markdown-extra",
|
||||
"string-extra": "extra/string-extra"
|
||||
},
|
||||
"defaults": {
|
||||
"git_constraint": "<1.8.2"
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,9 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
'needs_environment' => false,
|
||||
'needs_context' => false,
|
||||
'needs_charset' => false,
|
||||
'needs_is_sandboxed' => false,
|
||||
'is_variadic' => false,
|
||||
'always_allowed_in_sandbox' => false,
|
||||
'deprecation_info' => null,
|
||||
'deprecated' => false,
|
||||
'deprecating_package' => '',
|
||||
@@ -107,6 +109,16 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
|
||||
return $this->options['needs_context'];
|
||||
}
|
||||
|
||||
public function needsIsSandboxed(): bool
|
||||
{
|
||||
return $this->options['needs_is_sandboxed'];
|
||||
}
|
||||
|
||||
public function isAlwaysAllowedInSandbox(): bool
|
||||
{
|
||||
return $this->options['always_allowed_in_sandbox'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
@@ -182,6 +194,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,26 +31,30 @@ use Twig\TwigFilter;
|
||||
final class AsTwigFilter
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $name The name of the filter in Twig
|
||||
* @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 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
|
||||
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to
|
||||
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
|
||||
* @param non-empty-string $name The name of the filter in Twig
|
||||
* @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
|
||||
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to
|
||||
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
|
||||
* @param bool|null $alwaysAllowedInSandbox Whether the filter is always allowed in sandbox mode, even when not explicitly allow-listed
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
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,
|
||||
public ?array $preservesSafety = null,
|
||||
public ?DeprecatedCallableInfo $deprecationInfo = null,
|
||||
public ?bool $alwaysAllowedInSandbox = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,22 +31,26 @@ use Twig\TwigFunction;
|
||||
final class AsTwigFunction
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $name The name of the function in Twig
|
||||
* @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 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
|
||||
* @param non-empty-string $name The name of the function in Twig
|
||||
* @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
|
||||
* @param bool|null $alwaysAllowedInSandbox Whether the function is always allowed in sandbox mode, even when not explicitly allow-listed
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
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,
|
||||
public ?bool $alwaysAllowedInSandbox = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,18 +31,22 @@ use Twig\TwigTest;
|
||||
final class AsTwigTest
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $name The name of the test in Twig
|
||||
* @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 DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
|
||||
* @param non-empty-string $name The name of the test in Twig
|
||||
* @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
|
||||
* @param bool|null $alwaysAllowedInSandbox Whether the test is always allowed in sandbox mode, even when not explicitly allow-listed
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?bool $needsCharset = null,
|
||||
public ?bool $needsEnvironment = null,
|
||||
public ?bool $needsContext = null,
|
||||
public ?bool $needsIsSandboxed = null,
|
||||
public ?DeprecatedCallableInfo $deprecationInfo = null,
|
||||
public ?bool $alwaysAllowedInSandbox = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,13 @@ class Compiler
|
||||
*/
|
||||
public function string(string $value)
|
||||
{
|
||||
$this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
|
||||
// Single quotes are encoded as \x27 (not \') as a defense-in-depth measure:
|
||||
// it guarantees that the compiled output never contains a literal "'" derived
|
||||
// from user input, which prevents breaking out of a surrounding single-quoted
|
||||
// PHP context if a caller mistakenly concatenates the result into one.
|
||||
// \' is not a recognized escape sequence in PHP double-quoted strings (the
|
||||
// backslash would be kept literally), so \x27 is used instead.
|
||||
$this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\")));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ use Twig\TokenParser\TokenParserInterface;
|
||||
*/
|
||||
class Environment
|
||||
{
|
||||
public const VERSION = '3.23.0';
|
||||
public const VERSION_ID = 32300;
|
||||
public const VERSION = '3.28.0';
|
||||
public const VERSION_ID = 32800;
|
||||
public const MAJOR_VERSION = 3;
|
||||
public const MINOR_VERSION = 23;
|
||||
public const MINOR_VERSION = 28;
|
||||
public const RELEASE_VERSION = 0;
|
||||
public const EXTRA_VERSION = '';
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ use Twig\Template;
|
||||
class Error extends \Exception
|
||||
{
|
||||
private $lineno;
|
||||
/** @var positive-int|null */
|
||||
private ?int $columnno;
|
||||
private $rawMessage;
|
||||
private ?Source $source;
|
||||
private string $phpFile;
|
||||
@@ -46,17 +48,19 @@ class Error extends \Exception
|
||||
*
|
||||
* By default, automatic guessing is enabled.
|
||||
*
|
||||
* @param string $message The error message
|
||||
* @param int $lineno The template line where the error occurred
|
||||
* @param Source|null $source The source context where the error occurred
|
||||
* @param string $message The error message
|
||||
* @param int $lineno The template line where the error occurred
|
||||
* @param Source|null $source The source context where the error occurred
|
||||
* @param positive-int|null $columnno The template column where the error occurred
|
||||
*/
|
||||
public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null)
|
||||
public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null, ?int $columnno = null)
|
||||
{
|
||||
parent::__construct('', 0, $previous);
|
||||
|
||||
$this->phpFile = $this->getFile();
|
||||
$this->phpLine = $this->getLine();
|
||||
$this->lineno = $lineno;
|
||||
$this->columnno = $columnno;
|
||||
$this->source = $source;
|
||||
$this->rawMessage = $message;
|
||||
$this->updateRepr();
|
||||
@@ -78,6 +82,25 @@ class Error extends \Exception
|
||||
$this->updateRepr();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 1-based column where the error occurred, or null if unknown.
|
||||
*
|
||||
* @return positive-int|null
|
||||
*/
|
||||
public function getTemplateColumn(): ?int
|
||||
{
|
||||
return $this->columnno;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param positive-int|null $columnno
|
||||
*/
|
||||
public function setTemplateColumn(?int $columnno): void
|
||||
{
|
||||
$this->columnno = $columnno;
|
||||
$this->updateRepr();
|
||||
}
|
||||
|
||||
public function getSourceContext(): ?Source
|
||||
{
|
||||
return $this->source;
|
||||
@@ -127,6 +150,9 @@ class Error extends \Exception
|
||||
}
|
||||
if ($this->lineno > 0) {
|
||||
$this->message .= \sprintf(' at line %d', $this->lineno);
|
||||
if (null !== $this->columnno) {
|
||||
$this->message .= \sprintf(' column %d', $this->columnno);
|
||||
}
|
||||
}
|
||||
if ($punctuation) {
|
||||
$this->message .= $punctuation;
|
||||
|
||||
@@ -27,4 +27,9 @@ abstract class AbstractExpressionParser implements ExpressionParserInterface
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [$this->getName(), ...$this->getAliases()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
/**
|
||||
* @method list<string> getOperatorTokens() Returns the operator token strings that this expression parser handles.
|
||||
* These are the strings that should be recognized as operator tokens by the Lexer,
|
||||
* and used to look up the parser in the registry.
|
||||
* For most parsers, this returns the name and aliases. Parsers that don't handle
|
||||
* operator tokens (like LiteralExpressionParser) should return an empty array.
|
||||
* This method will be added to the interface in Twig 4.0.
|
||||
*/
|
||||
interface ExpressionParserInterface
|
||||
{
|
||||
public function __toString(): string;
|
||||
|
||||
@@ -54,10 +54,9 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
// throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()));
|
||||
}
|
||||
$interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class;
|
||||
$this->parsersByName[$interface][$parser->getName()] = $parser;
|
||||
$this->parsersByClass[$parser::class] = $parser;
|
||||
foreach ($parser->getAliases() as $alias) {
|
||||
$this->parsersByName[$interface][$alias] = $parser;
|
||||
foreach (self::getOperatorTokensFor($parser) as $token) {
|
||||
$this->parsersByName[$interface][$token] = $parser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +89,22 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
$seen = [];
|
||||
foreach ($this->parsersByName as $parsers) {
|
||||
// we don't yield the keys
|
||||
yield from $parsers;
|
||||
foreach ($parsers as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($this->parsersByClass as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +136,20 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
|
||||
return $this->precedenceChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getOperatorTokensFor(ExpressionParserInterface $parser): array
|
||||
{
|
||||
if (method_exists($parser, 'getOperatorTokens')) {
|
||||
return $parser->getOperatorTokens();
|
||||
}
|
||||
|
||||
trigger_deprecation('twig/twig', '3.24', 'Not implementing the "getOperatorTokens()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $parser::class, ExpressionParserInterface::class);
|
||||
|
||||
return [$parser->getName(), ...$parser->getAliases()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,12 @@ class AssignmentExpressionParser extends BinaryOperatorExpressionParser
|
||||
if ($left instanceof ArrayExpression) {
|
||||
if ($left->isSequence()) {
|
||||
return new SequenceDestructuringSetBinary($left, $right, $token->getLine());
|
||||
} else {
|
||||
return new ObjectDestructuringSetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
} else {
|
||||
return new SetBinary($left, $right, $token->getLine());
|
||||
|
||||
return new ObjectDestructuringSetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
return new SetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -65,14 +65,23 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
||||
$arguments = $this->parseCallableArguments($parser, $token->getLine());
|
||||
}
|
||||
|
||||
if (
|
||||
$expr instanceof NameExpression
|
||||
$isMacroTarget = $expr instanceof NameExpression
|
||||
&& (
|
||||
null !== $parser->getImportedSymbol('template', $expr->getAttribute('name'))
|
||||
|| '_self' === $expr->getAttribute('name') && $attribute instanceof ConstantExpression
|
||||
)
|
||||
|| '_self' === $expr->getAttribute('name')
|
||||
);
|
||||
|
||||
if (
|
||||
$isMacroTarget
|
||||
&& $attribute instanceof ConstantExpression
|
||||
&& \is_string($name = $attribute->getAttribute('value'))
|
||||
&& preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $name)
|
||||
) {
|
||||
return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine());
|
||||
return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$name, $arguments, $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
if ($isMacroTarget && !$attribute instanceof ConstantExpression) {
|
||||
return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), $attribute, $arguments, $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $nullSafe);
|
||||
|
||||
@@ -54,7 +54,14 @@ final class FunctionExpressionParser extends AbstractExpressionParser implements
|
||||
$fakeNode = new EmptyNode($line);
|
||||
$fakeNode->setSourceContext($parser->getStream()->getSourceContext());
|
||||
|
||||
return ($function->getParserCallable())($parser, $fakeNode, $args, $line);
|
||||
$node = ($function->getParserCallable())($parser, $fakeNode, $args, $line);
|
||||
// remember the original function name so the sandbox can enforce
|
||||
// the `allowedFunctions` allow-list even though the parser callable
|
||||
// returned a specialized node (e.g. `parent`, `block`, `attribute`).
|
||||
$node->setAttribute('sandboxed_function_name', $name);
|
||||
$node->setAttribute('sandboxed_function', $function);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
if (!isset($this->readyNodes[$class = $function->getNodeClass()])) {
|
||||
|
||||
@@ -17,6 +17,7 @@ use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\PrefixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ListExpression;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
@@ -36,7 +37,7 @@ final class GroupingExpressionParser extends AbstractExpressionParser implements
|
||||
return $expr->setExplicitParentheses();
|
||||
}
|
||||
|
||||
return new ListExpression([$expr], $token->getLine());
|
||||
return new ListExpression([self::toAssignContextVariable($expr)], $token->getLine());
|
||||
}
|
||||
|
||||
// determine if we are parsing an arrow function arguments
|
||||
@@ -58,7 +59,16 @@ final class GroupingExpressionParser extends AbstractExpressionParser implements
|
||||
throw new SyntaxError('A list of variables must be followed by an arrow.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
|
||||
return new ListExpression($names, $token->getLine());
|
||||
return new ListExpression(array_map(self::toAssignContextVariable(...), $names), $token->getLine());
|
||||
}
|
||||
|
||||
private static function toAssignContextVariable(AbstractExpression $expr): AssignContextVariable
|
||||
{
|
||||
if (!$expr instanceof ContextVariable) {
|
||||
throw new SyntaxError('A list must only contain variables.', $expr->getTemplateLine(), $expr->getSourceContext());
|
||||
}
|
||||
|
||||
return $expr instanceof AssignContextVariable ? $expr : new AssignContextVariable($expr->getAttribute('name'), $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
|
||||
@@ -30,8 +30,6 @@ use Twig\Token;
|
||||
*/
|
||||
final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
private string $type = 'literal';
|
||||
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
@@ -41,41 +39,30 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
switch ($token->getValue()) {
|
||||
case 'true':
|
||||
case 'TRUE':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(true, $token->getLine());
|
||||
|
||||
case 'false':
|
||||
case 'FALSE':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(false, $token->getLine());
|
||||
|
||||
case 'none':
|
||||
case 'NONE':
|
||||
case 'null':
|
||||
case 'NULL':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(null, $token->getLine());
|
||||
|
||||
default:
|
||||
$this->type = 'variable';
|
||||
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
|
||||
// no break
|
||||
case $token->test(Token::NUMBER_TYPE):
|
||||
$stream->next();
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression($token->getValue(), $token->getLine());
|
||||
|
||||
case $token->test(Token::STRING_TYPE):
|
||||
case $token->test(Token::INTERPOLATION_START_TYPE):
|
||||
$this->type = 'string';
|
||||
|
||||
return $this->parseStringExpression($parser);
|
||||
|
||||
case $token->test(Token::PUNCTUATION_TYPE):
|
||||
@@ -96,7 +83,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
|
||||
// in this context, string operators are variable names
|
||||
$stream->next();
|
||||
$this->type = 'variable';
|
||||
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
@@ -109,7 +95,12 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->type;
|
||||
return 'literal';
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
@@ -153,8 +144,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
private function parseSequenceExpression(Parser $parser)
|
||||
{
|
||||
$this->type = 'sequence';
|
||||
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected');
|
||||
|
||||
@@ -185,8 +174,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
private function parseMappingExpression(Parser $parser)
|
||||
{
|
||||
$this->type = 'mapping';
|
||||
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
|
||||
|
||||
|
||||
@@ -95,11 +95,13 @@ 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,
|
||||
'pre_escape' => $attribute->preEscape,
|
||||
'preserves_safety' => $attribute->preservesSafety,
|
||||
'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
@@ -118,9 +120,11 @@ 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,
|
||||
'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
@@ -139,7 +143,9 @@ 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(),
|
||||
'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false,
|
||||
'deprecation_info' => $attribute->deprecationInfo,
|
||||
]);
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ use Twig\Node\Expression\Unary\NotUnary;
|
||||
use Twig\Node\Expression\Unary\PosUnary;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Node;
|
||||
use Twig\NodeVisitor\CorrectnessNodeVisitor;
|
||||
use Twig\Parser;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
@@ -134,7 +135,7 @@ final class CoreExtension extends AbstractExtension
|
||||
|
||||
private $dateFormats = ['F j, Y H:i', '%d days'];
|
||||
private $numberFormat = [0, '.', ','];
|
||||
private $timezone = null;
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* Sets the default format to be used by the date filter.
|
||||
@@ -258,19 +259,19 @@ final class CoreExtension extends AbstractExtension
|
||||
new TwigFilter('striptags', [self::class, 'striptags']),
|
||||
new TwigFilter('trim', [self::class, 'trim']),
|
||||
new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]),
|
||||
new TwigFilter('spaceless', [self::class, 'spaceless'], ['pre_escape' => 'html', 'is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]),
|
||||
|
||||
// array helpers
|
||||
new TwigFilter('join', [self::class, 'join']),
|
||||
new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]),
|
||||
new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true]),
|
||||
new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
|
||||
new TwigFilter('merge', [self::class, 'merge']),
|
||||
new TwigFilter('batch', [self::class, 'batch']),
|
||||
new TwigFilter('column', [self::class, 'column']),
|
||||
new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true]),
|
||||
new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true]),
|
||||
new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]),
|
||||
new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true]),
|
||||
new TwigFilter('column', [self::class, 'column'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
|
||||
new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
|
||||
new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
|
||||
new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
|
||||
new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true, 'needs_is_sandboxed' => true]),
|
||||
|
||||
// string/array filters
|
||||
new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]),
|
||||
@@ -310,25 +311,27 @@ final class CoreExtension extends AbstractExtension
|
||||
public function getTests(): array
|
||||
{
|
||||
return [
|
||||
new TwigTest('even', null, ['node_class' => EvenTest::class]),
|
||||
new TwigTest('odd', null, ['node_class' => OddTest::class]),
|
||||
new TwigTest('defined', null, ['node_class' => DefinedTest::class]),
|
||||
new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true]),
|
||||
new TwigTest('none', null, ['node_class' => NullTest::class]),
|
||||
new TwigTest('null', null, ['node_class' => NullTest::class]),
|
||||
new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]),
|
||||
new TwigTest('even', null, ['node_class' => EvenTest::class, 'always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('odd', null, ['node_class' => OddTest::class, 'always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('defined', null, ['node_class' => DefinedTest::class, 'always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true, 'always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('none', null, ['node_class' => NullTest::class, 'always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('null', null, ['node_class' => NullTest::class, 'always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true, 'always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('constant', null, ['node_class' => ConstantTest::class]),
|
||||
new TwigTest('empty', [self::class, 'testEmpty']),
|
||||
new TwigTest('iterable', 'is_iterable'),
|
||||
new TwigTest('sequence', [self::class, 'testSequence']),
|
||||
new TwigTest('mapping', [self::class, 'testMapping']),
|
||||
new TwigTest('true', null, ['node_class' => TrueTest::class]),
|
||||
new TwigTest('empty', [self::class, 'testEmpty'], ['always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('iterable', 'is_iterable', ['always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('sequence', [self::class, 'testSequence'], ['always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('mapping', [self::class, 'testMapping'], ['always_allowed_in_sandbox' => true]),
|
||||
new TwigTest('true', null, ['node_class' => TrueTest::class, 'always_allowed_in_sandbox' => true]),
|
||||
];
|
||||
}
|
||||
|
||||
public function getNodeVisitors(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
new CorrectnessNodeVisitor(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getExpressionParsers(): array
|
||||
@@ -1044,7 +1047,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function sort(Environment $env, $array, $arrow = null): array
|
||||
public static function sort(Environment $env, bool $isSandboxed, $array, $arrow = null): array
|
||||
{
|
||||
if ($array instanceof \Traversable) {
|
||||
$array = iterator_to_array($array);
|
||||
@@ -1053,7 +1056,7 @@ final class CoreExtension extends AbstractExtension
|
||||
}
|
||||
|
||||
if (null !== $arrow) {
|
||||
self::checkArrow($env, $arrow, 'sort', 'filter');
|
||||
self::checkArrow($isSandboxed, $arrow, 'sort', 'filter');
|
||||
|
||||
uasort($array, $arrow);
|
||||
} else {
|
||||
@@ -1128,9 +1131,9 @@ final class CoreExtension extends AbstractExtension
|
||||
}
|
||||
if ((int) $bTrim == $bTrim) {
|
||||
return $a <=> (int) $bTrim;
|
||||
} else {
|
||||
return (float) $a <=> (float) $bTrim;
|
||||
}
|
||||
|
||||
return (float) $a <=> (float) $bTrim;
|
||||
}
|
||||
if (\is_string($a) && \is_int($b)) {
|
||||
$aTrim = trim($a, " \t\n\r\v\f");
|
||||
@@ -1139,9 +1142,9 @@ final class CoreExtension extends AbstractExtension
|
||||
}
|
||||
if ((int) $aTrim == $aTrim) {
|
||||
return (int) $aTrim <=> $b;
|
||||
} else {
|
||||
return (float) $aTrim <=> (float) $b;
|
||||
}
|
||||
|
||||
return (float) $aTrim <=> (float) $b;
|
||||
}
|
||||
|
||||
// float <=> string
|
||||
@@ -1179,7 +1182,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*/
|
||||
public static function matches(string $regexp, ?string $str): int
|
||||
{
|
||||
set_error_handler(function ($t, $m) use ($regexp) {
|
||||
set_error_handler(static function ($t, $m) use ($regexp) {
|
||||
throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12));
|
||||
});
|
||||
try {
|
||||
@@ -1490,9 +1493,11 @@ final class CoreExtension extends AbstractExtension
|
||||
* @param bool $ignoreMissing Whether to ignore missing templates or not
|
||||
* @param bool $sandboxed Whether to sandbox the template or not
|
||||
*
|
||||
* @return string|Markup
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string
|
||||
public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
|
||||
{
|
||||
$alreadySandboxed = false;
|
||||
$sandbox = null;
|
||||
@@ -1519,11 +1524,9 @@ final class CoreExtension extends AbstractExtension
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($isSandboxed) {
|
||||
$loaded->unwrap()->checkSecurity();
|
||||
}
|
||||
$rendered = $loaded->render($variables);
|
||||
|
||||
return $loaded->render($variables);
|
||||
return '' === $rendered ? '' : new Markup($rendered, $env->getCharset());
|
||||
} finally {
|
||||
if ($isSandboxed && !$alreadySandboxed) {
|
||||
$sandbox->disableSandbox();
|
||||
@@ -1686,6 +1689,9 @@ final class CoreExtension extends AbstractExtension
|
||||
public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
|
||||
{
|
||||
$propertyNotAllowedError = null;
|
||||
if ($sandboxed && $item instanceof \Stringable) {
|
||||
$env->getExtension(SandboxExtension::class)->ensureToStringAllowed($item, $lineno, $source);
|
||||
}
|
||||
|
||||
// array
|
||||
if (Template::METHOD_CALL !== $type) {
|
||||
@@ -1695,6 +1701,10 @@ final class CoreExtension extends AbstractExtension
|
||||
try {
|
||||
$env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source);
|
||||
} catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
|
||||
// The methodCheck path expects $item to be a string; stringify it here
|
||||
// to avoid PHP 8.1+ implicit float-to-int deprecations on downstream
|
||||
// array key lookups (e.g. isset($cache[$class][$item])).
|
||||
$item = (string) $item;
|
||||
goto methodCheck;
|
||||
}
|
||||
}
|
||||
@@ -1788,10 +1798,6 @@ final class CoreExtension extends AbstractExtension
|
||||
|
||||
static $propertyCheckers = [];
|
||||
|
||||
if ($object instanceof \Closure && '__invoke' === $item) {
|
||||
return $isDefinedTest ? true : $object();
|
||||
}
|
||||
|
||||
if (isset($object->$item)
|
||||
|| ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item)
|
||||
) {
|
||||
@@ -1840,14 +1846,14 @@ final class CoreExtension extends AbstractExtension
|
||||
$classCache[$lcName = $lcMethods[$i]] = $method;
|
||||
|
||||
if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) {
|
||||
$name = substr($method, 3);
|
||||
$lcName = substr($lcName, 3);
|
||||
$prefixLength = 3;
|
||||
$lcName = substr($lcName, $prefixLength);
|
||||
} elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) {
|
||||
$name = substr($method, 2);
|
||||
$lcName = substr($lcName, 2);
|
||||
$prefixLength = 2;
|
||||
$lcName = substr($lcName, $prefixLength);
|
||||
} elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) {
|
||||
$name = substr($method, 3);
|
||||
$lcName = substr($lcName, 3);
|
||||
$prefixLength = 3;
|
||||
$lcName = substr($lcName, $prefixLength);
|
||||
if (\in_array('is'.$lcName, $lcMethods, true)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1855,8 +1861,11 @@ final class CoreExtension extends AbstractExtension
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip get() and is() methods (in which case, $name is empty)
|
||||
if ($name) {
|
||||
// skip get(), is() and has() methods (in which case, $lcName is empty)
|
||||
if ($lcName) {
|
||||
// camelCase name (e.g. getFooBar() -> fooBar)
|
||||
$name = $lcName[0].substr($method, $prefixLength + 1);
|
||||
|
||||
if (!isset($classCache[$name])) {
|
||||
$classCache[$name] = $method;
|
||||
}
|
||||
@@ -1946,7 +1955,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function column($array, $name, $index = null): array
|
||||
public static function column(Environment $env, bool $isSandboxed, $array, $name, $index = null): array
|
||||
{
|
||||
if (!is_iterable($array)) {
|
||||
throw new RuntimeError(\sprintf('The "column" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
|
||||
@@ -1956,6 +1965,21 @@ final class CoreExtension extends AbstractExtension
|
||||
$array = iterator_to_array($array);
|
||||
}
|
||||
|
||||
if ($isSandboxed) {
|
||||
// The sandbox might be enabled via a SourcePolicyInterface, in which case the SandboxExtension
|
||||
// would not consider the sandbox active without the current Source: $isSandboxed is already
|
||||
// computed against the call-site source, so check the policy directly to honor that decision.
|
||||
$policy = $env->getExtension(SandboxExtension::class)->getSecurityPolicy();
|
||||
foreach ($array as $item) {
|
||||
if (\is_object($item)) {
|
||||
$policy->checkPropertyAllowed($item, (string) $name);
|
||||
if (null !== $index) {
|
||||
$policy->checkPropertyAllowed($item, (string) $index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_column($array, $name, $index);
|
||||
}
|
||||
|
||||
@@ -1964,13 +1988,13 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function filter(Environment $env, $array, $arrow)
|
||||
public static function filter(Environment $env, bool $isSandboxed, $array, $arrow)
|
||||
{
|
||||
if (!is_iterable($array)) {
|
||||
throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array)));
|
||||
}
|
||||
|
||||
self::checkArrow($env, $arrow, 'filter', 'filter');
|
||||
self::checkArrow($isSandboxed, $arrow, 'filter', 'filter');
|
||||
|
||||
if (\is_array($array)) {
|
||||
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
|
||||
@@ -1985,13 +2009,13 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function find(Environment $env, $array, $arrow)
|
||||
public static function find(Environment $env, bool $isSandboxed, $array, $arrow)
|
||||
{
|
||||
if (!is_iterable($array)) {
|
||||
throw new RuntimeError(\sprintf('The "find" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
|
||||
}
|
||||
|
||||
self::checkArrow($env, $arrow, 'find', 'filter');
|
||||
self::checkArrow($isSandboxed, $arrow, 'find', 'filter');
|
||||
|
||||
foreach ($array as $k => $v) {
|
||||
if ($arrow($v, $k)) {
|
||||
@@ -2007,13 +2031,13 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function map(Environment $env, $array, $arrow)
|
||||
public static function map(Environment $env, bool $isSandboxed, $array, $arrow)
|
||||
{
|
||||
if (!is_iterable($array)) {
|
||||
throw new RuntimeError(\sprintf('The "map" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
|
||||
}
|
||||
|
||||
self::checkArrow($env, $arrow, 'map', 'filter');
|
||||
self::checkArrow($isSandboxed, $arrow, 'map', 'filter');
|
||||
|
||||
$r = [];
|
||||
foreach ($array as $k => $v) {
|
||||
@@ -2028,13 +2052,13 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function reduce(Environment $env, $array, $arrow, $initial = null)
|
||||
public static function reduce(Environment $env, bool $isSandboxed, $array, $arrow, $initial = null)
|
||||
{
|
||||
if (!is_iterable($array)) {
|
||||
throw new RuntimeError(\sprintf('The "reduce" filter expects a sequence or a mapping, got "%s".', get_debug_type($array)));
|
||||
}
|
||||
|
||||
self::checkArrow($env, $arrow, 'reduce', 'filter');
|
||||
self::checkArrow($isSandboxed, $arrow, 'reduce', 'filter');
|
||||
|
||||
$accumulator = $initial;
|
||||
foreach ($array as $key => $value) {
|
||||
@@ -2049,13 +2073,13 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function arraySome(Environment $env, $array, $arrow)
|
||||
public static function arraySome(Environment $env, $array, $arrow, bool $isSandboxed = false)
|
||||
{
|
||||
if (!is_iterable($array)) {
|
||||
throw new RuntimeError(\sprintf('The "has some" test expects a sequence or a mapping, got "%s".', get_debug_type($array)));
|
||||
}
|
||||
|
||||
self::checkArrow($env, $arrow, 'has some', 'operator');
|
||||
self::checkArrow($isSandboxed, $arrow, 'has some', 'operator');
|
||||
|
||||
foreach ($array as $k => $v) {
|
||||
if ($arrow($v, $k)) {
|
||||
@@ -2071,13 +2095,13 @@ final class CoreExtension extends AbstractExtension
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function arrayEvery(Environment $env, $array, $arrow)
|
||||
public static function arrayEvery(Environment $env, $array, $arrow, bool $isSandboxed = false)
|
||||
{
|
||||
if (!is_iterable($array)) {
|
||||
throw new RuntimeError(\sprintf('The "has every" test expects a sequence or a mapping, got "%s".', get_debug_type($array)));
|
||||
}
|
||||
|
||||
self::checkArrow($env, $arrow, 'has every', 'operator');
|
||||
self::checkArrow($isSandboxed, $arrow, 'has every', 'operator');
|
||||
|
||||
foreach ($array as $k => $v) {
|
||||
if (!$arrow($v, $k)) {
|
||||
@@ -2091,13 +2115,13 @@ final class CoreExtension extends AbstractExtension
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function checkArrow(Environment $env, $arrow, $thing, $type)
|
||||
public static function checkArrow(bool $isSandboxed, $arrow, $thing, $type)
|
||||
{
|
||||
if ($arrow instanceof \Closure) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) {
|
||||
if ($isSandboxed) {
|
||||
throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
|
||||
}
|
||||
|
||||
@@ -2148,7 +2172,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*/
|
||||
public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
|
||||
{
|
||||
$fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null);
|
||||
$fakeFunction = new TwigFunction('block', static fn ($name, $template = null) => null);
|
||||
$args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
|
||||
|
||||
return new BlockReferenceExpression($args[0], $args[1] ?? null, $line);
|
||||
@@ -2159,7 +2183,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*/
|
||||
public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
|
||||
{
|
||||
$fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null);
|
||||
$fakeFunction = new TwigFunction('attribute', static fn ($variable, $attribute, $arguments = null) => null);
|
||||
$args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
|
||||
|
||||
/*
|
||||
|
||||
@@ -76,7 +76,7 @@ final class EscaperExtension extends AbstractExtension
|
||||
}
|
||||
|
||||
$this->environment = $environment;
|
||||
$this->escaper = $environment->getRuntime(EscaperRuntime::class);
|
||||
$this->escaper = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,16 +140,14 @@ final class EscaperExtension extends AbstractExtension
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__);
|
||||
|
||||
if (!isset($this->environment)) {
|
||||
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
|
||||
}
|
||||
$escaper = $this->getEscaper(__METHOD__);
|
||||
|
||||
$this->escapers[$strategy] = $callable;
|
||||
$callable = function ($string, $charset) use ($callable) {
|
||||
return $callable($this->environment, $string, $charset);
|
||||
};
|
||||
|
||||
$this->escaper->setEscaper($strategy, $callable);
|
||||
$escaper->setEscaper($strategy, $callable);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,11 +173,7 @@ final class EscaperExtension extends AbstractExtension
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__);
|
||||
|
||||
if (!isset($this->escaper)) {
|
||||
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
|
||||
}
|
||||
|
||||
$this->escaper->setSafeClasses($safeClasses);
|
||||
$this->getEscaper(__METHOD__)->setSafeClasses($safeClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,11 +185,20 @@ final class EscaperExtension extends AbstractExtension
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__);
|
||||
|
||||
if (!isset($this->escaper)) {
|
||||
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
|
||||
$this->getEscaper(__METHOD__)->addSafeClass($class, $strategies);
|
||||
}
|
||||
|
||||
private function getEscaper(string $fromMethod): EscaperRuntime
|
||||
{
|
||||
if (isset($this->escaper)) {
|
||||
return $this->escaper;
|
||||
}
|
||||
|
||||
$this->escaper->addSafeClass($class, $strategies);
|
||||
if (isset($this->environment)) {
|
||||
return $this->escaper = $this->environment->getRuntime(EscaperRuntime::class);
|
||||
}
|
||||
|
||||
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', $fromMethod));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,10 @@ final class SandboxExtension extends AbstractExtension
|
||||
|
||||
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null)
|
||||
{
|
||||
if (null !== $sourcePolicy) {
|
||||
trigger_deprecation('twig/twig', '3.27.0', 'The "%s" interface is deprecated with no replacement, do not pass an instance to "%s".', SourcePolicyInterface::class, self::class);
|
||||
}
|
||||
|
||||
$this->policy = $policy;
|
||||
$this->sandboxedGlobally = $sandboxed;
|
||||
$this->sourcePolicy = $sourcePolicy;
|
||||
@@ -82,11 +86,29 @@ final class SandboxExtension extends AbstractExtension
|
||||
return $this->policy;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void
|
||||
public function checkSecurity($tags, $filters, $functions, $tests = [], $source = null): void
|
||||
{
|
||||
if ($this->isSandboxed($source)) {
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
// BC: previous signature was checkSecurity($tags, $filters, $functions, ?Source $source = null);
|
||||
// detect a legacy call where the 4th positional argument was the Source.
|
||||
if ($tests instanceof Source || (null === $tests && \func_num_args() < 5)) {
|
||||
trigger_deprecation('twig/twig', '3.28', 'Passing a "Twig\Source" as the 4th argument of "%s()" is deprecated; pass an array of tests instead.', __METHOD__);
|
||||
$source = $tests;
|
||||
$tests = [];
|
||||
}
|
||||
|
||||
if (!$this->isSandboxed($source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((new \ReflectionMethod($this->policy, 'checkSecurity'))->getNumberOfParameters() >= 4) {
|
||||
$this->policy->checkSecurity($tags, $filters, $functions, $tests);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
trigger_deprecation('twig/twig', '3.28', 'The "%s::checkSecurity()" method will take a 4th "array $tests" argument in 4.0; not declaring it is deprecated.', $this->policy::class);
|
||||
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void
|
||||
@@ -121,14 +143,43 @@ final class SandboxExtension extends AbstractExtension
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
|
||||
{
|
||||
return $this->doEnsureToStringAllowed($obj, $lineno, $source, new \SplObjectStorage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialises a spread operand and runs the policy on every element.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureSpreadAllowed(iterable $obj, int $lineno = -1, ?Source $source = null): array
|
||||
{
|
||||
$seen = new \SplObjectStorage();
|
||||
if ($obj instanceof \Traversable) {
|
||||
$seen[$obj] = true;
|
||||
$obj = iterator_to_array($obj);
|
||||
}
|
||||
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
private function doEnsureToStringAllowed($obj, int $lineno, ?Source $source, \SplObjectStorage $seen)
|
||||
{
|
||||
if (\is_array($obj)) {
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source);
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
if ($obj instanceof \Stringable && $this->isSandboxed($source)) {
|
||||
if (!$this->isSandboxed($source)) {
|
||||
return $obj;
|
||||
}
|
||||
|
||||
if ($obj instanceof \Stringable) {
|
||||
try {
|
||||
$this->policy->checkMethodAllowed($obj, '__toString');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
@@ -139,10 +190,38 @@ final class SandboxExtension extends AbstractExtension
|
||||
}
|
||||
}
|
||||
|
||||
// Elements yielded by a Traversable may be string-coerced downstream
|
||||
// (e.g. by `join`/`replace`), bypassing the policy. Check them now.
|
||||
if ($obj instanceof \Traversable) {
|
||||
if (isset($seen[$obj])) {
|
||||
return $obj;
|
||||
}
|
||||
$seen[$obj] = true;
|
||||
|
||||
// IteratorAggregate::getIterator() is idempotent, so we can walk
|
||||
// the elements and return the original object: host code typed
|
||||
// against a specific class (e.g. FormView) keeps working.
|
||||
if ($obj instanceof \IteratorAggregate) {
|
||||
foreach ($obj as $v) {
|
||||
$this->doEnsureToStringAllowed($v, $lineno, $source, $seen);
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
// Single-pass Iterator/Generator: materialise to validate.
|
||||
$array = iterator_to_array($obj);
|
||||
$this->ensureToStringAllowedForArray($array, $lineno, $source, $seen);
|
||||
|
||||
if (!$obj instanceof \Stringable) {
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void
|
||||
private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, \SplObjectStorage $seen, array &$stack = []): void
|
||||
{
|
||||
foreach ($obj as $k => $v) {
|
||||
if (!$v) {
|
||||
@@ -150,7 +229,7 @@ final class SandboxExtension extends AbstractExtension
|
||||
}
|
||||
|
||||
if (!\is_array($v)) {
|
||||
$this->ensureToStringAllowed($v, $lineno, $source);
|
||||
$this->doEnsureToStringAllowed($v, $lineno, $source, $seen);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -162,7 +241,7 @@ final class SandboxExtension extends AbstractExtension
|
||||
$stack[$r->getId()] = true;
|
||||
}
|
||||
|
||||
$this->ensureToStringAllowedForArray($v, $lineno, $source, $stack);
|
||||
$this->ensureToStringAllowedForArray($v, $lineno, $source, $seen, $stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ final class StringLoaderExtension extends AbstractExtension
|
||||
*
|
||||
* {{ include(template_from_string("Hello {{ name }}")) }}
|
||||
*
|
||||
* Never expose `template_from_string` to untrusted template
|
||||
* authors (like in a sandboxed environment). See the docs for more details.
|
||||
*
|
||||
* @param string|null $name An optional name of the template to be used in error messages
|
||||
*
|
||||
* @internal
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\ExpressionParsers;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
@@ -61,6 +62,8 @@ class Lexer
|
||||
public const REGEX_INLINE_COMMENT = '/#[^\n]*/A';
|
||||
public const PUNCTUATION = '()[]{}?:.,|';
|
||||
|
||||
private const REGEX_RAW_INLINE_COMMENT = '/#[^\r\n]*/A';
|
||||
|
||||
private const SPECIAL_CHARS = [
|
||||
'f' => "\f",
|
||||
'n' => "\n",
|
||||
@@ -112,7 +115,7 @@ class Lexer
|
||||
'|'.
|
||||
preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
|
||||
'|'.
|
||||
preg_quote($this->options['tag_block'][1], '#').'\n?'. // %}\n?
|
||||
preg_quote($this->options['tag_block'][1], '#').'(?:\r\n?|\n)?'. // %}(?:\r\n?|\n)?
|
||||
')
|
||||
}Ax',
|
||||
|
||||
@@ -142,7 +145,7 @@ class Lexer
|
||||
'|'.
|
||||
preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]*
|
||||
'|'.
|
||||
preg_quote($this->options['tag_comment'][1], '#').'\n?'. // #}\n?
|
||||
preg_quote($this->options['tag_comment'][1], '#').'(?:\r\n?|\n)?'. // #}(?:\r\n?|\n)?
|
||||
')
|
||||
}sx',
|
||||
|
||||
@@ -186,7 +189,7 @@ class Lexer
|
||||
$this->initialize();
|
||||
|
||||
$this->source = $source;
|
||||
$this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
|
||||
$this->code = $source->getCode();
|
||||
$this->cursor = 0;
|
||||
$this->lineno = 1;
|
||||
$this->end = \strlen($this->code);
|
||||
@@ -240,8 +243,9 @@ class Lexer
|
||||
{
|
||||
// if no matches are left we return the rest of the template as simple text token
|
||||
if ($this->position == \count($this->positions[0]) - 1) {
|
||||
$this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor));
|
||||
$this->cursor = $this->end;
|
||||
$text = substr($this->code, $this->cursor);
|
||||
$this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text));
|
||||
$this->moveCursor($text);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -269,15 +273,20 @@ class Lexer
|
||||
$text = rtrim($text, " \t\0\x0B");
|
||||
}
|
||||
}
|
||||
$this->pushToken(Token::TEXT_TYPE, $text);
|
||||
$this->moveCursor($textContent.$position[0]);
|
||||
$this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text));
|
||||
$this->moveCursor($textContent);
|
||||
|
||||
switch ($this->positions[1][$this->position][0]) {
|
||||
case $this->options['tag_comment'][0]:
|
||||
$this->moveCursor($position[0]);
|
||||
$this->lexComment();
|
||||
break;
|
||||
|
||||
case $this->options['tag_block'][0]:
|
||||
$lineno = $this->lineno;
|
||||
$cursor = $this->cursor;
|
||||
$this->moveCursor($position[0]);
|
||||
|
||||
// raw data?
|
||||
if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) {
|
||||
$this->moveCursor($match[0]);
|
||||
@@ -287,14 +296,17 @@ class Lexer
|
||||
$this->moveCursor($match[0]);
|
||||
$this->lineno = (int) $match[1];
|
||||
} else {
|
||||
$this->pushToken(Token::BLOCK_START_TYPE);
|
||||
$this->pushToken(Token::BLOCK_START_TYPE, '', $cursor, $lineno);
|
||||
$this->pushState(self::STATE_BLOCK);
|
||||
$this->currentVarBlockLine = $this->lineno;
|
||||
}
|
||||
break;
|
||||
|
||||
case $this->options['tag_variable'][0]:
|
||||
$this->pushToken(Token::VAR_START_TYPE);
|
||||
$lineno = $this->lineno;
|
||||
$cursor = $this->cursor;
|
||||
$this->moveCursor($position[0]);
|
||||
$this->pushToken(Token::VAR_START_TYPE, '', $cursor, $lineno);
|
||||
$this->pushState(self::STATE_VAR);
|
||||
$this->currentVarBlockLine = $this->lineno;
|
||||
break;
|
||||
@@ -304,8 +316,7 @@ class Lexer
|
||||
private function lexBlock(): void
|
||||
{
|
||||
if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
|
||||
$this->pushToken(Token::BLOCK_END_TYPE);
|
||||
$this->moveCursor($match[0]);
|
||||
$this->pushClosingToken(Token::BLOCK_END_TYPE, $match[0]);
|
||||
$this->popState();
|
||||
} else {
|
||||
$this->lexExpression();
|
||||
@@ -315,8 +326,7 @@ class Lexer
|
||||
private function lexVar(): void
|
||||
{
|
||||
if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
|
||||
$this->pushToken(Token::VAR_END_TYPE);
|
||||
$this->moveCursor($match[0]);
|
||||
$this->pushClosingToken(Token::VAR_END_TYPE, $match[0]);
|
||||
$this->popState();
|
||||
} else {
|
||||
$this->lexExpression();
|
||||
@@ -357,11 +367,11 @@ class Lexer
|
||||
elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) {
|
||||
$this->checkBrackets($this->code[$this->cursor]);
|
||||
$this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]);
|
||||
++$this->cursor;
|
||||
$this->moveCursor($this->code[$this->cursor]);
|
||||
}
|
||||
// strings
|
||||
elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
|
||||
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1)));
|
||||
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($this->normalizeNewlines(substr($match[0], 1, -1)), substr($match[0], 0, 1)));
|
||||
$this->moveCursor($match[0]);
|
||||
}
|
||||
// opening double quoted string
|
||||
@@ -371,12 +381,12 @@ class Lexer
|
||||
$this->moveCursor($match[0]);
|
||||
}
|
||||
// inline comment
|
||||
elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
|
||||
elseif (preg_match(self::REGEX_RAW_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
|
||||
$this->moveCursor($match[0]);
|
||||
}
|
||||
// unlexable
|
||||
else {
|
||||
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
|
||||
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +435,7 @@ class Lexer
|
||||
while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) {
|
||||
$octal .= $str[++$i];
|
||||
}
|
||||
$result .= \chr(octdec($octal));
|
||||
$result .= \chr(octdec($octal) % 256);
|
||||
} else {
|
||||
trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno);
|
||||
$result .= $nextChar;
|
||||
@@ -443,6 +453,7 @@ class Lexer
|
||||
throw new SyntaxError('Unexpected end of file: Unclosed "verbatim" block.', $this->lineno, $this->source);
|
||||
}
|
||||
|
||||
$offset = $this->cursor;
|
||||
$text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
|
||||
$this->moveCursor($text.$match[0][0]);
|
||||
|
||||
@@ -458,7 +469,7 @@ class Lexer
|
||||
}
|
||||
}
|
||||
|
||||
$this->pushToken(Token::TEXT_TYPE, $text);
|
||||
$this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text), $offset);
|
||||
}
|
||||
|
||||
private function lexComment(): void
|
||||
@@ -478,7 +489,7 @@ class Lexer
|
||||
$this->moveCursor($match[0]);
|
||||
$this->pushState(self::STATE_INTERPOLATION);
|
||||
} elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) {
|
||||
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"'));
|
||||
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($this->normalizeNewlines($match[0]), '"'));
|
||||
$this->moveCursor($match[0]);
|
||||
} elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
|
||||
[$expect, $lineno] = array_pop($this->brackets);
|
||||
@@ -487,10 +498,10 @@ class Lexer
|
||||
}
|
||||
|
||||
$this->popState();
|
||||
++$this->cursor;
|
||||
$this->moveCursor($match[0]);
|
||||
} else {
|
||||
// unlexable
|
||||
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
|
||||
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,35 +510,54 @@ class Lexer
|
||||
$bracket = end($this->brackets);
|
||||
if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
|
||||
array_pop($this->brackets);
|
||||
$this->pushToken(Token::INTERPOLATION_END_TYPE);
|
||||
$this->moveCursor($match[0]);
|
||||
$this->pushClosingToken(Token::INTERPOLATION_END_TYPE, $match[0]);
|
||||
$this->popState();
|
||||
} else {
|
||||
$this->lexExpression();
|
||||
}
|
||||
}
|
||||
|
||||
private function pushToken($type, $value = ''): void
|
||||
private function pushToken($type, $value = '', ?int $offset = null, ?int $lineno = null): void
|
||||
{
|
||||
// do not push empty text tokens
|
||||
if (Token::TEXT_TYPE === $type && '' === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tokens[] = new Token($type, $value, $this->lineno);
|
||||
// by default the token starts at the current cursor; callers that
|
||||
// emit a token after consuming it must pass an explicit offset
|
||||
$this->tokens[] = new Token($type, $value, $lineno ?? $this->lineno, $offset ?? $this->cursor);
|
||||
}
|
||||
|
||||
private function moveCursor($text): void
|
||||
{
|
||||
$this->cursor += \strlen($text);
|
||||
$this->lineno += substr_count($text, "\n");
|
||||
// count "\r\n" and "\r" as a single newline without allocating a
|
||||
// normalized copy when the chunk has no carriage return (common case)
|
||||
$this->lineno += str_contains($text, "\r") ? substr_count($this->normalizeNewlines($text), "\n") : substr_count($text, "\n");
|
||||
}
|
||||
|
||||
private function normalizeNewlines(string $text): string
|
||||
{
|
||||
return str_replace(["\r\n", "\r"], "\n", $text);
|
||||
}
|
||||
|
||||
private function pushClosingToken(int $type, string $match): void
|
||||
{
|
||||
$leadingWhitespaceLength = \strlen($match) - \strlen(ltrim($match));
|
||||
if ($leadingWhitespaceLength) {
|
||||
$this->moveCursor(substr($match, 0, $leadingWhitespaceLength));
|
||||
}
|
||||
|
||||
$this->pushToken($type);
|
||||
$this->moveCursor(substr($match, $leadingWhitespaceLength));
|
||||
}
|
||||
|
||||
private function getOperatorRegex(): string
|
||||
{
|
||||
$expressionParsers = [];
|
||||
foreach ($this->env->getExpressionParsers() as $expressionParser) {
|
||||
$expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases());
|
||||
$expressionParsers = array_merge($expressionParsers, ExpressionParsers::getOperatorTokensFor($expressionParser));
|
||||
}
|
||||
|
||||
$expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers));
|
||||
@@ -544,7 +574,7 @@ class Lexer
|
||||
|
||||
// an operator that begins with a character must not have a dot or pipe before
|
||||
if (ctype_alpha($expressionParser[0])) {
|
||||
$r = '(?<![\.\|])'.$r;
|
||||
$r = '(?<![\.\|]\s|.[\.\|])'.$r;
|
||||
}
|
||||
|
||||
// an operator with a space can be any amount of whitespaces
|
||||
@@ -579,7 +609,7 @@ class Lexer
|
||||
} elseif (\in_array($code, $this->closingBrackets, true)) {
|
||||
// closing bracket
|
||||
if (!$this->brackets) {
|
||||
throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source);
|
||||
throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));
|
||||
}
|
||||
|
||||
[$expect, $lineno] = array_pop($this->brackets);
|
||||
|
||||
@@ -14,6 +14,16 @@ namespace Twig;
|
||||
/**
|
||||
* Marks a content as safe.
|
||||
*
|
||||
* Instances of this class (and existing subclasses) are trusted by the Twig
|
||||
* sandbox: method calls and property accesses on a Markup instance bypass the
|
||||
* SecurityPolicy method/property allowlists. This is by design: Markup
|
||||
* represents content that has already been deemed safe to output.
|
||||
*
|
||||
* This class is considered final as of Twig 3.28 and will be final in Twig
|
||||
* 4.0.
|
||||
*
|
||||
* @final since Twig 3.28
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Markup implements \Countable, \JsonSerializable, \Stringable
|
||||
|
||||
@@ -27,7 +27,6 @@ class CheckSecurityCallNode extends Node
|
||||
{
|
||||
$compiler
|
||||
->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n")
|
||||
->write("\$this->checkSecurity();\n")
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,24 @@ class CheckSecurityNode extends Node
|
||||
private $usedFilters;
|
||||
private $usedTags;
|
||||
private $usedFunctions;
|
||||
private $usedTests;
|
||||
|
||||
/**
|
||||
* @param array<string, int> $usedFilters
|
||||
* @param array<string, int> $usedTags
|
||||
* @param array<string, int> $usedFunctions
|
||||
* @param array<string, int> $usedTests
|
||||
*/
|
||||
public function __construct(array $usedFilters, array $usedTags, array $usedFunctions)
|
||||
public function __construct(array $usedFilters, array $usedTags, array $usedFunctions, array $usedTests = [])
|
||||
{
|
||||
if (\func_num_args() < 4) {
|
||||
trigger_deprecation('twig/twig', '3.28', 'Not passing the "$usedTests" argument to "%s::__construct()" is deprecated; it will be required in 4.0.', static::class);
|
||||
}
|
||||
|
||||
$this->usedFilters = $usedFilters;
|
||||
$this->usedTags = $usedTags;
|
||||
$this->usedFunctions = $usedFunctions;
|
||||
$this->usedTests = $usedTests;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -41,20 +48,33 @@ class CheckSecurityNode extends Node
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler
|
||||
->write("\n")
|
||||
->write("public function ensureSecurityChecked(): void\n")
|
||||
->write("{\n")
|
||||
->indent()
|
||||
->write("if (\$this->sandbox->isSandboxed(\$this->source)) {\n")
|
||||
->indent()
|
||||
->write("\$this->checkSecurity();\n")
|
||||
->outdent()
|
||||
->write("}\n")
|
||||
->outdent()
|
||||
->write("}\n")
|
||||
->write("\n")
|
||||
->write("public function checkSecurity()\n")
|
||||
->write("{\n")
|
||||
->indent()
|
||||
->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n")
|
||||
->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n")
|
||||
->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n")
|
||||
->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n")
|
||||
->write('static $tests = ')->repr(array_filter($this->usedTests))->raw(";\n\n")
|
||||
->write("try {\n")
|
||||
->indent()
|
||||
->write("\$this->sandbox->checkSecurity(\n")
|
||||
->indent()
|
||||
->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n")
|
||||
->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n")
|
||||
->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n")
|
||||
->write('')->repr(array_keys($this->usedTags))->raw(",\n")
|
||||
->write('')->repr(array_keys($this->usedFilters))->raw(",\n")
|
||||
->write('')->repr(array_keys($this->usedFunctions))->raw(",\n")
|
||||
->write('')->repr(array_keys($this->usedTests))->raw(",\n")
|
||||
->write("\$this->source\n")
|
||||
->outdent()
|
||||
->write(");\n")
|
||||
@@ -74,6 +94,10 @@ class CheckSecurityNode extends Node
|
||||
->indent()
|
||||
->write("\$e->setTemplateLine(\$functions[\$e->getFunctionName()]);\n")
|
||||
->outdent()
|
||||
->write("} elseif (\$e instanceof SecurityNotAllowedTestError && isset(\$tests[\$e->getTestName()])) {\n")
|
||||
->indent()
|
||||
->write("\$e->setTemplateLine(\$tests[\$e->getTestName()]);\n")
|
||||
->outdent()
|
||||
->write("}\n\n")
|
||||
->write("throw \$e;\n")
|
||||
->outdent()
|
||||
|
||||
@@ -28,16 +28,17 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
#[YieldReady]
|
||||
class CheckToStringNode extends AbstractExpression
|
||||
{
|
||||
public function __construct(AbstractExpression $expr)
|
||||
public function __construct(AbstractExpression $expr, bool $spread = false)
|
||||
{
|
||||
parent::__construct(['expr' => $expr], [], $expr->getTemplateLine());
|
||||
parent::__construct(['expr' => $expr], ['spread' => $spread], $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$expr = $this->getNode('expr');
|
||||
$method = $this->getAttribute('spread') ? 'ensureSpreadAllowed' : 'ensureToStringAllowed';
|
||||
$compiler
|
||||
->raw('$this->sandbox->ensureToStringAllowed(')
|
||||
->raw('$this->sandbox->'.$method.'(')
|
||||
->subcompile($expr)
|
||||
->raw(', ')
|
||||
->repr($expr->getTemplateLine())
|
||||
|
||||
40
lib/twig/twig/src/Node/CoercesChildrenToStringInterface.php
Normal file
40
lib/twig/twig/src/Node/CoercesChildrenToStringInterface.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node;
|
||||
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
|
||||
/**
|
||||
* Implemented by nodes that implicitly coerce one or more of their child
|
||||
* nodes to string at runtime (PHP string casts, regex matching, comparisons,
|
||||
* range bounds, template-name resolution by the loader, etc.).
|
||||
*
|
||||
* The sandbox node visitor wraps the listed children with a CheckToStringNode
|
||||
* so that an implicit `__toString()` call goes through the sandbox policy
|
||||
* check, independently of where this node's result is used.
|
||||
*
|
||||
* This is distinct from {@see OperatorEscapeInterface}, which describes
|
||||
* operands whose value becomes this expression's value (passthrough operators
|
||||
* like ternaries) and is consumed by the escaper.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface CoercesChildrenToStringInterface
|
||||
{
|
||||
/**
|
||||
* Returns the names of the child nodes that will be coerced to
|
||||
* string when this node is evaluated.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getStringCoercedChildNames(): array;
|
||||
}
|
||||
30
lib/twig/twig/src/Node/ConfigNode.php
Normal file
30
lib/twig/twig/src/Node/ConfigNode.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Node;
|
||||
|
||||
use Twig\Attribute\YieldReady;
|
||||
|
||||
/**
|
||||
* Represents a node that has global side effects but does not generate template code.
|
||||
*
|
||||
* Such nodes must be at the root level of the body of a template.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
final class ConfigNode extends Node
|
||||
{
|
||||
public function __construct(int $lineno)
|
||||
{
|
||||
parent::__construct([], [], $lineno);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ use Twig\Node\Expression\ConstantExpression;
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class DeprecatedNode extends Node
|
||||
class DeprecatedNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, int $lineno)
|
||||
{
|
||||
@@ -70,4 +70,18 @@ class DeprecatedNode extends Node
|
||||
->raw(");\n")
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the message is concatenated with `.`, and `package` / `version` are typed `string` on trigger_deprecation()
|
||||
$names = ['expr'];
|
||||
if ($this->hasNode('package')) {
|
||||
$names[] = 'package';
|
||||
}
|
||||
if ($this->hasNode('version')) {
|
||||
$names[] = 'version';
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Unary\StringCastUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
|
||||
class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface, ReturnArrayInterface
|
||||
class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface, ReturnArrayInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
@@ -95,6 +95,24 @@ class ArrayExpression extends AbstractExpression implements SupportDefinedTestIn
|
||||
array_push($this->nodes, $key, $value);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// dynamic mapping keys (computed at runtime) are coerced to string;
|
||||
// static keys (constants or sequence indexes) are emitted as PHP
|
||||
// literals by compile() and never trigger a __toString() call
|
||||
$names = [];
|
||||
foreach (array_chunk($this->nodes, 2) as $i => $pair) {
|
||||
$key = $pair[0];
|
||||
if ($key instanceof ConstantExpression || $key instanceof TempNameExpression) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$names[] = (string) ($i * 2);
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->definedTest) {
|
||||
@@ -118,13 +136,15 @@ class ArrayExpression extends AbstractExpression implements SupportDefinedTestIn
|
||||
}
|
||||
|
||||
$key = null;
|
||||
if ($pair['key'] instanceof ContextVariable) {
|
||||
$pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
|
||||
} elseif ($pair['key'] instanceof TempNameExpression) {
|
||||
if ($pair['key'] instanceof TempNameExpression) {
|
||||
$key = $pair['key']->getAttribute('name');
|
||||
$pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
|
||||
} elseif ($pair['key'] instanceof ConstantExpression) {
|
||||
$key = $pair['key']->getAttribute('value');
|
||||
} else {
|
||||
// dynamic key: cast to string so PHP accepts it as an array offset
|
||||
// (the sandbox visitor has already wrapped it with a __toString policy check)
|
||||
$pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
|
||||
}
|
||||
|
||||
if ($key !== $i) {
|
||||
|
||||
@@ -26,14 +26,14 @@ class ArrowFunctionExpression extends AbstractExpression
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, Node $names, $lineno)
|
||||
{
|
||||
if (!$names instanceof ListExpression && !$names instanceof ContextVariable) {
|
||||
throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext());
|
||||
}
|
||||
|
||||
if ($names instanceof ContextVariable) {
|
||||
$names = new ListExpression([new AssignContextVariable($names->getAttribute('name'), $names->getTemplateLine())], $lineno);
|
||||
}
|
||||
|
||||
if (!$names instanceof ListExpression) {
|
||||
throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext());
|
||||
}
|
||||
|
||||
parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,16 @@ namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class AndBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('&&');
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnStringInterface;
|
||||
|
||||
class ConcatBinary extends AbstractBinary implements ReturnStringInterface
|
||||
class ConcatBinary extends AbstractBinary implements ReturnStringInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('.');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Twig\Node\Expression\Binary;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterface
|
||||
@@ -26,7 +27,7 @@ final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterfac
|
||||
{
|
||||
parent::__construct($left, $right, $lineno);
|
||||
|
||||
$this->setNode('test', clone $left);
|
||||
$this->setNode('test', TrueTest::wrap(clone $left));
|
||||
$left->setAttribute('always_defined', true);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class EqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class EqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class EqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('==');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class GreaterBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class GreaterBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class GreaterBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('>');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('>=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class HasEveryBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
->subcompile($this->getNode('left'))
|
||||
->raw(', ')
|
||||
->subcompile($this->getNode('right'))
|
||||
->raw(')')
|
||||
->raw(', $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source))')
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class HasSomeBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
->subcompile($this->getNode('left'))
|
||||
->raw(', ')
|
||||
->subcompile($this->getNode('right'))
|
||||
->raw(')')
|
||||
->raw(', $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source))')
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class InBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class InBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -31,4 +32,9 @@ class InBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('in');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class LessBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class LessBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class LessBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('<');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('<=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,23 @@ namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class MatchesBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class MatchesBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
if (!$left instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" first argument is deprecated, pass an "AbstractExpression" instance instead.', $left::class, __METHOD__);
|
||||
}
|
||||
if (!$right instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" second argument is deprecated, pass an "AbstractExpression" instance instead.', $right::class, __METHOD__);
|
||||
}
|
||||
|
||||
if ($right instanceof ConstantExpression) {
|
||||
$regexp = $right->getAttribute('value');
|
||||
set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid: %s.', $regexp, substr($m, 14)), $lineno));
|
||||
@@ -49,4 +58,9 @@ class MatchesBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('!=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class NotInBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class NotInBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -31,4 +32,9 @@ class NotInBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('not in');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ final class NullCoalesceBinary extends AbstractBinary implements OperatorEscapeI
|
||||
{
|
||||
parent::__construct($left, $right, $lineno);
|
||||
|
||||
$test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine());
|
||||
$test = new DefinedTest(clone $left, new TwigTest('defined', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine());
|
||||
// for "block()", we don't need the null test as the return value is always a string
|
||||
if (!$left instanceof BlockReferenceExpression) {
|
||||
$test = new AndBinary(
|
||||
$test,
|
||||
new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
|
||||
new NotUnary(new NullTest($left, new TwigTest('null', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
|
||||
$left->getTemplateLine(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
@@ -23,7 +24,8 @@ use Twig\Node\Node;
|
||||
*/
|
||||
class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
{
|
||||
private array $properties = [];
|
||||
/** @var list<array{property: string, variable: string}> */
|
||||
private array $mappings = [];
|
||||
|
||||
/**
|
||||
* @param ArrayExpression $left The array expression containing object/mapping destructuring properties
|
||||
@@ -38,7 +40,11 @@ class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
if (!$pair['value'] instanceof ContextVariable) {
|
||||
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno);
|
||||
}
|
||||
$this->properties[] = $pair['value']->getAttribute('name');
|
||||
|
||||
$this->mappings[] = [
|
||||
'property' => $pair['key']->getAttribute('value'),
|
||||
'variable' => $pair['value']->getAttribute('name'),
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($left, $right, $lineno);
|
||||
@@ -48,18 +54,18 @@ class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
{
|
||||
$compiler->addDebugInfo($this);
|
||||
$compiler->raw('[');
|
||||
foreach ($this->properties as $i => $property) {
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$compiler->raw('$context[')->repr($property)->raw(']');
|
||||
$compiler->raw('$context[')->repr($mapping['variable'])->raw(']');
|
||||
}
|
||||
$compiler->raw('] = [');
|
||||
foreach ($this->properties as $i => $property) {
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ')->subcompile($this->getNode('right'))->raw(', ')->repr($property)->raw(', [], \\Twig\\Template::ANY_CALL, false, false, false, ')->repr($this->getNode('right')->getTemplateLine())->raw(')');
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ')->subcompile($this->getNode('right'))->raw(', ')->repr($mapping['property'])->raw(', [], \\Twig\\Template::ANY_CALL, false, false, ')->repr($compiler->getEnvironment()->hasExtension(SandboxExtension::class))->raw(', ')->repr($this->getNode('right')->getTemplateLine())->raw(')');
|
||||
}
|
||||
$compiler->raw(']');
|
||||
}
|
||||
|
||||
@@ -14,9 +14,16 @@ namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class OrBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('||');
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnArrayInterface;
|
||||
|
||||
class RangeBinary extends AbstractBinary implements ReturnArrayInterface
|
||||
class RangeBinary extends AbstractBinary implements ReturnArrayInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -31,4 +32,9 @@ class RangeBinary extends AbstractBinary implements ReturnArrayInterface
|
||||
{
|
||||
return $compiler->raw('..');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,18 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class SpaceshipBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
class SpaceshipBinary extends AbstractBinary implements ReturnNumberInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('<=>');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,16 @@ namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class XorBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('xor');
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Node;
|
||||
|
||||
/**
|
||||
@@ -20,7 +21,7 @@ use Twig\Node\Node;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
@@ -60,6 +61,12 @@ class BlockReferenceExpression extends AbstractExpression implements SupportDefi
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the template expression is resolved through the loader, which coerces it to a string
|
||||
return $this->hasNode('template') ? ['template'] : [];
|
||||
}
|
||||
|
||||
private function compileTemplateCall(Compiler $compiler, string $method): Compiler
|
||||
{
|
||||
if (!$this->hasNode('template')) {
|
||||
|
||||
@@ -24,7 +24,7 @@ use Twig\Util\ReflectionCallable;
|
||||
|
||||
abstract class CallExpression extends AbstractExpression
|
||||
{
|
||||
private $reflector = null;
|
||||
private $reflector;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
@@ -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(', ');
|
||||
@@ -213,9 +221,8 @@ abstract class CallExpression extends AbstractExpression
|
||||
} elseif ($callableParameter->isOptional()) {
|
||||
if (!$parameters) {
|
||||
break;
|
||||
} else {
|
||||
$missingArguments[] = $name;
|
||||
}
|
||||
$missingArguments[] = $name;
|
||||
} else {
|
||||
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
|
||||
}
|
||||
@@ -275,25 +282,9 @@ abstract class CallExpression extends AbstractExpression
|
||||
{
|
||||
$twigCallable = $this->getAttribute('twig_callable');
|
||||
$rc = $this->reflectCallable($twigCallable);
|
||||
$r = $rc->getReflector();
|
||||
$callableName = $rc->getName();
|
||||
|
||||
$parameters = $r->getParameters();
|
||||
if ($this->hasNode('node')) {
|
||||
array_shift($parameters);
|
||||
}
|
||||
if ($twigCallable->needsCharset()) {
|
||||
array_shift($parameters);
|
||||
}
|
||||
if ($twigCallable->needsEnvironment()) {
|
||||
array_shift($parameters);
|
||||
}
|
||||
if ($twigCallable->needsContext()) {
|
||||
array_shift($parameters);
|
||||
}
|
||||
foreach ($twigCallable->getArguments() as $argument) {
|
||||
array_shift($parameters);
|
||||
}
|
||||
$parameters = $rc->getTwigParameters($this->hasNode('node'));
|
||||
|
||||
$isPhpVariadic = false;
|
||||
if ($isVariadic) {
|
||||
@@ -321,6 +312,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).
|
||||
*
|
||||
@@ -335,6 +342,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()),
|
||||
@@ -345,6 +353,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()),
|
||||
@@ -355,6 +364,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()),
|
||||
|
||||
@@ -54,7 +54,7 @@ class DefaultFilter extends FilterExpression
|
||||
}
|
||||
|
||||
if ('default' === $name && ($node instanceof ContextVariable || $node instanceof GetAttrExpression)) {
|
||||
$test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine());
|
||||
$test = new DefinedTest(clone $node, new TwigTest('defined', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $node->getTemplateLine());
|
||||
$false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine());
|
||||
|
||||
$node = new ConditionalTernary($test, $default, $false, $node->getTemplateLine());
|
||||
|
||||
@@ -14,11 +14,12 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\NameDeprecation;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
class FilterExpression extends CallExpression
|
||||
class FilterExpression extends CallExpression implements CoercesChildrenToStringInterface
|
||||
{
|
||||
/**
|
||||
* @param AbstractExpression $node
|
||||
@@ -77,4 +78,10 @@ class FilterExpression extends CallExpression
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// a filter may coerce its input and arguments to string (e.g. `upper`, `replace`)
|
||||
return ['node', 'arguments'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\NameDeprecation;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class FunctionExpression extends CallExpression implements SupportDefinedTestInterface
|
||||
class FunctionExpression extends CallExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
@@ -78,4 +79,10 @@ class FunctionExpression extends CallExpression implements SupportDefinedTestInt
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// a function may coerce its arguments to string (the host PHP code is opaque to Twig)
|
||||
return ['arguments'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Template;
|
||||
|
||||
class GetAttrExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
class GetAttrExpression extends AbstractExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
@@ -36,7 +37,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
|
||||
}
|
||||
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe], $lineno);
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe, 'is_short_circuited' => false, 'var_name' => null], $lineno);
|
||||
}
|
||||
|
||||
public function enableDefinedTest(): void
|
||||
@@ -50,7 +51,6 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$env = $compiler->getEnvironment();
|
||||
$arrayAccessSandbox = false;
|
||||
$nullSafe = $this->getAttribute('null_safe');
|
||||
$objectVar = null;
|
||||
|
||||
// optimize array calls
|
||||
if (
|
||||
@@ -73,9 +73,9 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
->raw(' instanceof ArrayAccess ? (')
|
||||
->raw($var)
|
||||
->raw('[')
|
||||
->subcompile($this->getNode('attribute'))
|
||||
->raw('] ?? null) : null)')
|
||||
;
|
||||
$this->compileArrayKey($compiler);
|
||||
$compiler->raw('] ?? null) : null)');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -90,27 +90,41 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (')
|
||||
->raw($var)
|
||||
->raw('[')
|
||||
->subcompile($this->getNode('attribute'))
|
||||
->raw('] ?? null) : ')
|
||||
;
|
||||
$this->compileArrayKey($compiler);
|
||||
$compiler->raw('] ?? null) : ');
|
||||
}
|
||||
|
||||
if ($this->getAttribute('ignore_strict_check')) {
|
||||
$this->getNode('node')->setAttribute('ignore_strict_check', true);
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
$objectVar = '$'.$compiler->getVarName();
|
||||
if (null === $nullSafeNode = $nullSafe ? $this : null) {
|
||||
$node = $this->getNode('node');
|
||||
while ($node instanceof self) {
|
||||
if ($node->getAttribute('null_safe')) {
|
||||
$nullSafeNode = $node;
|
||||
break;
|
||||
}
|
||||
$node = $node->getNode('node');
|
||||
}
|
||||
}
|
||||
|
||||
$isShortCircuited = false;
|
||||
if (null !== $nullSafeNode && !$nullSafeNode->isShortCircuited()) {
|
||||
$compiler
|
||||
->raw('((null === ('.$objectVar.' = ')
|
||||
->subcompile($this->getNode('node'))
|
||||
->raw('((null === ('.$nullSafeNode->getVarName($compiler).' = ')
|
||||
->subcompile($nullSafeNode->getNode('node'))
|
||||
->raw(')) ? null : ');
|
||||
|
||||
$nullSafeNode->markAsShortCircuited();
|
||||
$isShortCircuited = true;
|
||||
}
|
||||
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
||||
|
||||
if ($nullSafe) {
|
||||
$compiler->raw($objectVar);
|
||||
$compiler->raw($this->getVarName($compiler));
|
||||
} else {
|
||||
$compiler->subcompile($this->getNode('node'));
|
||||
}
|
||||
@@ -139,11 +153,50 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$compiler->raw(')');
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
if ($isShortCircuited) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
$names = [];
|
||||
|
||||
// the host PHP method may coerce any argument to string
|
||||
if ($this->hasNode('arguments')) {
|
||||
$names[] = 'arguments';
|
||||
}
|
||||
|
||||
// compileArrayKey() coerces a Stringable key; expose it so the sandbox checks __toString()
|
||||
if (Template::ARRAY_CALL === $this->getAttribute('type')) {
|
||||
$names[] = 'attribute';
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a Stringable array key to string so the optimized path matches
|
||||
* CoreExtension::getAttribute(); scalars are left to PHP's native offset coercion.
|
||||
*/
|
||||
private function compileArrayKey(Compiler $compiler): void
|
||||
{
|
||||
$attribute = $this->getNode('attribute');
|
||||
|
||||
if ($attribute instanceof ConstantExpression) {
|
||||
$compiler->subcompile($attribute);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$key = '$'.$compiler->getVarName();
|
||||
$compiler
|
||||
->raw('(('.$key.' = ')
|
||||
->subcompile($attribute)
|
||||
->raw(') instanceof \Stringable ? (string) '.$key.' : '.$key.')')
|
||||
;
|
||||
}
|
||||
|
||||
private function changeIgnoreStrictCheck(self $node): void
|
||||
{
|
||||
$node->setAttribute('optimizable', false);
|
||||
@@ -153,4 +206,23 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$this->changeIgnoreStrictCheck($node->getNode('node'));
|
||||
}
|
||||
}
|
||||
|
||||
private function markAsShortCircuited(): void
|
||||
{
|
||||
$this->setAttribute('is_short_circuited', true);
|
||||
}
|
||||
|
||||
private function isShortCircuited(): bool
|
||||
{
|
||||
return $this->getAttribute('is_short_circuited');
|
||||
}
|
||||
|
||||
private function getVarName(Compiler $compiler): string
|
||||
{
|
||||
if (null === $this->getAttribute('var_name')) {
|
||||
$this->setAttribute('var_name', $compiler->getVarName());
|
||||
}
|
||||
|
||||
return '$'.$this->getAttribute('var_name');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
|
||||
class ListExpression extends AbstractExpression
|
||||
{
|
||||
/**
|
||||
* @param array<ContextVariable> $items
|
||||
* @param array<AssignContextVariable> $items
|
||||
*/
|
||||
public function __construct(array $items, int $lineno)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\Variable\TemplateVariable;
|
||||
|
||||
/**
|
||||
@@ -19,18 +20,55 @@ use Twig\Node\Expression\Variable\TemplateVariable;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class MacroReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
class MacroReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno)
|
||||
/**
|
||||
* @param string|AbstractExpression $name A static macro method name (e.g. "macro_foo") or, for a dynamic
|
||||
* call, an expression resolving to the macro name (without the
|
||||
* "macro_" prefix, which is added at runtime)
|
||||
*/
|
||||
public function __construct(TemplateVariable $template, string|AbstractExpression $name, AbstractExpression $arguments, int $lineno)
|
||||
{
|
||||
parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name], $lineno);
|
||||
$nodes = ['template' => $template, 'arguments' => $arguments];
|
||||
$attributes = ['name' => null];
|
||||
|
||||
if (\is_string($name)) {
|
||||
// The name is emitted as raw PHP in compile() via "->{$name}(...)",
|
||||
// so it must be a valid PHP method identifier. Reject anything else
|
||||
// as a defense-in-depth against accidental PHP code injection from
|
||||
// a caller that forgot to validate user-controlled input.
|
||||
if (!preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $name)) {
|
||||
throw new \LogicException(\sprintf('Macro name "%s" is not a valid PHP identifier.', $name));
|
||||
}
|
||||
$attributes['name'] = $name;
|
||||
} else {
|
||||
$nodes['name'] = $name;
|
||||
}
|
||||
|
||||
parent::__construct($nodes, $attributes, $lineno);
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
// The template node must not be deep-cloned because its name is
|
||||
// lazily generated during compilation and must stay in sync with
|
||||
// the AssignTemplateVariable that populates the $macros array.
|
||||
$template = $this->nodes['template'];
|
||||
parent::__clone();
|
||||
$this->nodes['template'] = $template;
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->hasNode('name')) {
|
||||
$this->compileDynamic($compiler);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->definedTest) {
|
||||
$compiler
|
||||
->subcompile($this->getNode('template'))
|
||||
@@ -56,4 +94,43 @@ class MacroReferenceExpression extends AbstractExpression implements SupportDefi
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// Dynamic macro names are prefixed via PHP string concatenation at runtime.
|
||||
return $this->hasNode('name') ? ['name'] : [];
|
||||
}
|
||||
|
||||
private function compileDynamic(Compiler $compiler): void
|
||||
{
|
||||
// The macro method name is resolved at runtime from a context value;
|
||||
// prefixing it with "macro_" constrains the dynamic method call to the
|
||||
// template's macro methods only, and getTemplateForMacro()/hasMacro()
|
||||
// validate that the method actually exists.
|
||||
$var = $compiler->getVarName();
|
||||
|
||||
if ($this->definedTest) {
|
||||
$compiler
|
||||
->subcompile($this->getNode('template'))
|
||||
->raw('->hasMacro(\'macro_\'.')
|
||||
->subcompile($this->getNode('name'))
|
||||
->raw(', $context)')
|
||||
;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$compiler
|
||||
->subcompile($this->getNode('template'))
|
||||
->raw(\sprintf('->getTemplateForMacro($%s = \'macro_\'.', $var))
|
||||
->subcompile($this->getNode('name'))
|
||||
->raw(', $context, ')
|
||||
->repr($this->getTemplateLine())
|
||||
->raw(', $this->getSourceContext())')
|
||||
->raw(\sprintf('->{$%s}', $var))
|
||||
->raw('(...')
|
||||
->subcompile($this->getNode('arguments'))
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ class NullCoalesceExpression extends ConditionalExpression
|
||||
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $right::class);
|
||||
}
|
||||
|
||||
$test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine());
|
||||
$test = new DefinedTest(clone $left, new TwigTest('defined', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine());
|
||||
// for "block()", we don't need the null test as the return value is always a string
|
||||
if (!$left instanceof BlockReferenceExpression) {
|
||||
$test = new AndBinary(
|
||||
$test,
|
||||
new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
|
||||
new NotUnary(new NullTest($left, new TwigTest('null', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
|
||||
$left->getTemplateLine()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,19 +14,13 @@ namespace Twig\Node\Expression\Ternary;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
use Twig\Node\Expression\ReturnPrimitiveTypeInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\TwigTest;
|
||||
|
||||
final class ConditionalTernary extends AbstractExpression implements OperatorEscapeInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $test, AbstractExpression $left, AbstractExpression $right, int $lineno)
|
||||
{
|
||||
if (!$test instanceof ReturnPrimitiveTypeInterface) {
|
||||
$test = new TrueTest($test, new TwigTest('true'), null, $test->getTemplateLine());
|
||||
}
|
||||
|
||||
parent::__construct(['test' => $test, 'left' => $left, 'right' => $right], [], $lineno);
|
||||
parent::__construct(['test' => TrueTest::wrap($test), 'left' => $left, 'right' => $right], [], $lineno);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
|
||||
@@ -59,4 +59,10 @@ class DefinedTest extends TestExpression
|
||||
{
|
||||
$compiler->subcompile($this->getNode('node'));
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the `defined` test does not coerce its node to string (it only inspects existence)
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,10 @@ class DivisiblebyTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// PHP `%` rejects Stringable with a TypeError, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,10 @@ class EvenTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// PHP `%` rejects Stringable with a TypeError, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,10 @@ class NullTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// `=== null` is strict, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,10 @@ class OddTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// PHP `%` rejects Stringable with a TypeError, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,10 @@ class SameasTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// `===` is strict, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
namespace Twig\Node\Expression\Test;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnPrimitiveTypeInterface;
|
||||
use Twig\Node\Expression\TestExpression;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* Checks that an expression is true.
|
||||
@@ -23,6 +26,15 @@ use Twig\Node\Expression\TestExpression;
|
||||
*/
|
||||
class TrueTest extends TestExpression
|
||||
{
|
||||
public static function wrap(Node $node): Node
|
||||
{
|
||||
if ($node instanceof ReturnPrimitiveTypeInterface) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
return new self($node, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $node->getTemplateLine());
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler
|
||||
@@ -31,4 +43,10 @@ class TrueTest extends TestExpression
|
||||
->raw(') && $tmp instanceof Markup ? (string) $tmp : $tmp)')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the `(string)` cast only fires for Markup instances, whose __toString is always allowed
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\NameDeprecation;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigTest;
|
||||
|
||||
class TestExpression extends CallExpression implements ReturnBoolInterface
|
||||
class TestExpression extends CallExpression implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
#[FirstClassTwigCallableReady]
|
||||
/**
|
||||
@@ -70,4 +71,21 @@ class TestExpression extends CallExpression implements ReturnBoolInterface
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
$names = [];
|
||||
|
||||
// the `empty` test triggers an implicit string coercion through `CoreExtension::testEmpty()`
|
||||
if ('empty' === $this->getAttribute('name')) {
|
||||
$names[] = 'node';
|
||||
}
|
||||
|
||||
// a test may coerce its arguments to string (the host PHP code is opaque to Twig)
|
||||
if ($this->hasNode('arguments')) {
|
||||
$names[] = 'arguments';
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,17 @@
|
||||
namespace Twig\Node\Expression\Unary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class NotUnary extends AbstractUnary
|
||||
class NotUnary extends AbstractUnary implements ReturnBoolInterface
|
||||
{
|
||||
public function __construct(Node $node, int $lineno)
|
||||
{
|
||||
parent::__construct(TrueTest::wrap($node), $lineno);
|
||||
}
|
||||
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('!');
|
||||
|
||||
@@ -115,6 +115,9 @@ class ForNode extends Node
|
||||
$compiler->raw(");\n");
|
||||
|
||||
// keep the values set in the inner context for variables defined in the outer context
|
||||
$compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n");
|
||||
$compiler
|
||||
->write("\$context = array_intersect_key(\$context, \$_parent);\n")
|
||||
->write("\$context += \$_parent;\n")
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ namespace Twig\Node;
|
||||
|
||||
use Twig\Attribute\YieldReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\ReturnPrimitiveTypeInterface;
|
||||
use Twig\Node\Expression\Test\TrueTest;
|
||||
use Twig\TwigTest;
|
||||
|
||||
/**
|
||||
* Represents an if node.
|
||||
@@ -29,10 +27,7 @@ class IfNode extends Node
|
||||
public function __construct(Node $tests, ?Node $else, int $lineno)
|
||||
{
|
||||
for ($i = 0, $count = \count($tests); $i < $count; $i += 2) {
|
||||
$test = $tests->getNode((string) $i);
|
||||
if (!$test instanceof ReturnPrimitiveTypeInterface) {
|
||||
$tests->setNode($i, new TrueTest($test, new TwigTest('true'), null, $test->getTemplateLine()));
|
||||
}
|
||||
$tests->setNode($i, TrueTest::wrap($tests->getNode((string) $i)));
|
||||
}
|
||||
$nodes = ['tests' => $tests];
|
||||
if (null !== $else) {
|
||||
|
||||
@@ -23,7 +23,7 @@ use Twig\Node\Expression\Variable\ContextVariable;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class ImportNode extends Node
|
||||
class ImportNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno)
|
||||
{
|
||||
@@ -58,4 +58,10 @@ class ImportNode extends Node
|
||||
|
||||
$compiler->raw(";\n");
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the loader resolves the template-name expression by coercing it to a string
|
||||
return ['expr'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class IncludeNode extends Node implements NodeOutputInterface
|
||||
class IncludeNode extends Node implements NodeOutputInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno)
|
||||
{
|
||||
@@ -60,9 +60,10 @@ class IncludeNode extends Node implements NodeOutputInterface
|
||||
->write("}\n")
|
||||
->write(\sprintf("if ($%s) {\n", $template))
|
||||
->indent()
|
||||
->write(\sprintf('yield from $%s->unwrap()->yield(', $template))
|
||||
;
|
||||
|
||||
$compiler->write(\sprintf('yield from $%s->unwrap()->yield(', $template));
|
||||
|
||||
$this->addTemplateArguments($compiler);
|
||||
$compiler
|
||||
->raw(");\n")
|
||||
@@ -111,4 +112,10 @@ class IncludeNode extends Node implements NodeOutputInterface
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the loader resolves the template-name expression by coercing it to a string
|
||||
return ['expr'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use Twig\Source;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
final class ModuleNode extends Node
|
||||
final class ModuleNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
/**
|
||||
* @param BodyNode $body
|
||||
@@ -90,6 +90,12 @@ final class ModuleNode extends Node
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the parent expression is resolved through the loader, which coerces it to a string
|
||||
return $this->hasNode('parent') ? ['parent'] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -178,6 +184,7 @@ final class ModuleNode extends Node
|
||||
->write("use Twig\Sandbox\SecurityNotAllowedTagError;\n")
|
||||
->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n")
|
||||
->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n")
|
||||
->write("use Twig\Sandbox\SecurityNotAllowedTestError;\n")
|
||||
->write("use Twig\Source;\n")
|
||||
->write("use Twig\Template;\n")
|
||||
->write("use Twig\TemplateWrapper;\n")
|
||||
@@ -248,11 +255,11 @@ final class ModuleNode extends Node
|
||||
->string($key)
|
||||
->raw("])) {\n")
|
||||
->indent()
|
||||
->write("throw new RuntimeError('Block ")
|
||||
->write("throw new RuntimeError(sprintf('Block \"%s\" is not defined in trait \"%s\".', ")
|
||||
->string($key)
|
||||
->raw(' is not defined in trait ')
|
||||
->raw(', ')
|
||||
->subcompile($trait->getNode('template'))
|
||||
->raw(".', ")
|
||||
->raw('), ')
|
||||
->repr($node->getTemplateLine())
|
||||
->raw(", \$this->source);\n")
|
||||
->outdent()
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Twig\Node;
|
||||
use Twig\Attribute\YieldReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\ReturnStringInterface;
|
||||
|
||||
/**
|
||||
* Represents a node that outputs an expression.
|
||||
@@ -22,7 +24,7 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class PrintNode extends Node implements NodeOutputInterface
|
||||
class PrintNode extends Node implements NodeOutputInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, int $lineno)
|
||||
{
|
||||
@@ -34,11 +36,34 @@ class PrintNode extends Node implements NodeOutputInterface
|
||||
/** @var AbstractExpression */
|
||||
$expr = $this->getNode('expr');
|
||||
|
||||
$compiler->addDebugInfo($this);
|
||||
|
||||
if ($expr->isGenerator()) {
|
||||
$compiler->write('yield from ');
|
||||
} else {
|
||||
$compiler->write('yield ');
|
||||
if (!$this->isString($expr)) {
|
||||
$compiler->raw('(string) ');
|
||||
}
|
||||
}
|
||||
|
||||
$compiler
|
||||
->addDebugInfo($this)
|
||||
->write($expr->isGenerator() ? 'yield from ' : 'yield ')
|
||||
->subcompile($expr)
|
||||
->raw(";\n")
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['expr'];
|
||||
}
|
||||
|
||||
private function isString(AbstractExpression $expr): bool
|
||||
{
|
||||
if ($expr instanceof ReturnStringInterface) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $expr instanceof ConstantExpression && !$expr->isDefinedTestEnabled() && \is_string($expr->getAttribute('value'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,20 @@ class TextNode extends Node implements NodeOutputInterface
|
||||
->raw(";\n")
|
||||
;
|
||||
}
|
||||
|
||||
public function isBlank(): bool
|
||||
{
|
||||
if (ctype_space($this->getAttribute('data'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_contains((string) $this, \chr(0xEF).\chr(0xBB).\chr(0xBF))) {
|
||||
$t = substr($this->getAttribute('data'), 3);
|
||||
if ('' === $t || ctype_space($t)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
223
lib/twig/twig/src/NodeVisitor/CorrectnessNodeVisitor.php
Normal file
223
lib/twig/twig/src/NodeVisitor/CorrectnessNodeVisitor.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\NodeVisitor;
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\BlockNode;
|
||||
use Twig\Node\BlockReferenceNode;
|
||||
use Twig\Node\ConfigNode;
|
||||
use Twig\Node\MacroNode;
|
||||
use Twig\Node\ModuleNode;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\NodeCaptureInterface;
|
||||
use Twig\Node\NodeOutputInterface;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Node\TextNode;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CorrectnessNodeVisitor implements NodeVisitorInterface
|
||||
{
|
||||
private ?\WeakMap $rootNodes = null;
|
||||
/**
|
||||
* Stack of the output-wrapping tags ("if", "for", "set", ...) currently open;
|
||||
* the top one is the nearest tag a "block" definition would be nested under.
|
||||
*
|
||||
* @var list<Node>
|
||||
*/
|
||||
private array $tagStack = [];
|
||||
private bool $hasParent = false;
|
||||
private int $blockDepth = 0;
|
||||
private int $macroDepth = 0;
|
||||
private int $capturingNodeDepth = 0;
|
||||
private bool $hasExtends = false;
|
||||
|
||||
public function enterNode(Node $node, Environment $env): Node
|
||||
{
|
||||
if ($node instanceof ModuleNode) {
|
||||
$this->enterModule($node);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
$this->enterScope($node);
|
||||
|
||||
if ($node instanceof ConfigNode) {
|
||||
$this->checkConfigTag($node);
|
||||
}
|
||||
|
||||
if ($node instanceof BlockReferenceNode) {
|
||||
$this->checkBlockDefinitionNesting($node);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
public function leaveNode(Node $node, Environment $env): Node
|
||||
{
|
||||
if ($node instanceof ModuleNode) {
|
||||
$this->resetState();
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
$this->leaveScope($node);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return -255;
|
||||
}
|
||||
|
||||
private function enterModule(ModuleNode $node): void
|
||||
{
|
||||
$this->resetState();
|
||||
$this->rootNodes = new \WeakMap();
|
||||
$this->hasParent = $node->hasNode('parent');
|
||||
|
||||
foreach ($this->getRootNodes($node) as $n) {
|
||||
if ($this->hasParent && !$this->isEmptyOutputNode($n)) {
|
||||
throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $n->getTemplateLine(), $n->getSourceContext());
|
||||
}
|
||||
$this->rootNodes[$n] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private function resetState(): void
|
||||
{
|
||||
$this->rootNodes = null;
|
||||
$this->tagStack = [];
|
||||
$this->hasParent = false;
|
||||
$this->blockDepth = 0;
|
||||
$this->macroDepth = 0;
|
||||
$this->capturingNodeDepth = 0;
|
||||
$this->hasExtends = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Node>
|
||||
*/
|
||||
private function getRootNodes(ModuleNode $node): iterable
|
||||
{
|
||||
$body = $node->getNode('body')->getNode('0');
|
||||
|
||||
// Parser::subparse() does not wrap the parsed nodes when there is only one,
|
||||
// so $body can be a single "real" node instead of a Nodes container; in that
|
||||
// case it is the only root node and must not be iterated over its children.
|
||||
return $body instanceof Nodes || Node::class === $body::class ? $body : [$body];
|
||||
}
|
||||
|
||||
private function enterScope(Node $node): void
|
||||
{
|
||||
if ($node instanceof NodeCaptureInterface) {
|
||||
++$this->capturingNodeDepth;
|
||||
}
|
||||
|
||||
if ($node instanceof BlockNode) {
|
||||
++$this->blockDepth;
|
||||
} elseif ($node instanceof MacroNode) {
|
||||
++$this->macroDepth;
|
||||
} elseif ($node->getNodeTag() && !$node instanceof BlockReferenceNode) {
|
||||
$this->tagStack[] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
private function leaveScope(Node $node): void
|
||||
{
|
||||
if ($node instanceof NodeCaptureInterface) {
|
||||
--$this->capturingNodeDepth;
|
||||
}
|
||||
|
||||
if ($node instanceof BlockNode) {
|
||||
--$this->blockDepth;
|
||||
} elseif ($node instanceof MacroNode) {
|
||||
--$this->macroDepth;
|
||||
} elseif ($node->getNodeTag() && !$node instanceof BlockReferenceNode) {
|
||||
array_pop($this->tagStack);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkConfigTag(ConfigNode $node): void
|
||||
{
|
||||
if ('extends' === $node->getNodeTag()) {
|
||||
$this->checkExtendsTag($node);
|
||||
}
|
||||
|
||||
if (!isset($this->rootNodes[$node])) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'Using the "%s" tag outside the root of a template is deprecated in %s at line %d.', $node->getNodeTag(), $node->getSourceContext()->getName(), $node->getTemplateLine());
|
||||
}
|
||||
}
|
||||
|
||||
private function checkExtendsTag(ConfigNode $node): void
|
||||
{
|
||||
// "extends" inside a "block" or a "macro" has always been a hard error; keep it
|
||||
if ($this->blockDepth) {
|
||||
throw new SyntaxError('Cannot use "extend" in a block.', $node->getTemplateLine(), $node->getSourceContext());
|
||||
}
|
||||
if ($this->macroDepth) {
|
||||
throw new SyntaxError('Cannot use "extend" in a macro.', $node->getTemplateLine(), $node->getSourceContext());
|
||||
}
|
||||
if ($this->hasExtends) {
|
||||
throw new SyntaxError('Multiple extends tags are forbidden.', $node->getTemplateLine(), $node->getSourceContext());
|
||||
}
|
||||
|
||||
$this->hasExtends = true;
|
||||
}
|
||||
|
||||
private function checkBlockDefinitionNesting(BlockReferenceNode $node): void
|
||||
{
|
||||
// A "block" definition nested under an output-wrapping tag is registered globally
|
||||
// regardless of that tag, so the nesting is misleading. This only matters at the root
|
||||
// of a child template's body: once inside a block, a macro, an output capture, or in
|
||||
// a standalone template, the block is rendered in place and behaves like any other.
|
||||
if (!$this->hasParent || $this->blockDepth || $this->macroDepth || $this->capturingNodeDepth || !$this->tagStack) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tag = $this->tagStack[array_key_last($this->tagStack)];
|
||||
throw new SyntaxError(\sprintf('A "block" tag cannot be under a "%s" tag (line %d).', $tag->getNodeTag(), $tag->getTemplateLine()), $node->getTemplateLine(), $node->getSourceContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the node never outputs anything or if the output is empty.
|
||||
*/
|
||||
private function isEmptyOutputNode(Node $node): bool
|
||||
{
|
||||
if ($node instanceof NodeCaptureInterface) {
|
||||
// a "block" tag in such a node will serve as a block definition AND be displayed in place as well
|
||||
return true;
|
||||
}
|
||||
|
||||
// Can the text be considered "empty" (only whitespace)?
|
||||
if ($node instanceof TextNode) {
|
||||
return $node->isBlank();
|
||||
}
|
||||
|
||||
if (!$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($node as $n) {
|
||||
if (!$this->isEmptyOutputNode($n)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,11 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
if (\in_array('html_attr', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
$bucket['value'][] = 'html_attr_relaxed';
|
||||
}
|
||||
|
||||
if (\in_array('html_attr_relaxed', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
}
|
||||
|
||||
return $bucket['value'];
|
||||
|
||||
@@ -15,19 +15,22 @@ use Twig\Environment;
|
||||
use Twig\Node\CheckSecurityCallNode;
|
||||
use Twig\Node\CheckSecurityNode;
|
||||
use Twig\Node\CheckToStringNode;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\ConcatBinary;
|
||||
use Twig\Node\Expression\Binary\RangeBinary;
|
||||
use Twig\Node\Expression\FilterExpression;
|
||||
use Twig\Node\Expression\FunctionExpression;
|
||||
use Twig\Node\Expression\GetAttrExpression;
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
use Twig\Node\Expression\TestExpression;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\ModuleNode;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Node\PrintNode;
|
||||
use Twig\Node\SetNode;
|
||||
use Twig\TokenParser\TokenParserInterface;
|
||||
use Twig\TwigCallableInterface;
|
||||
use Twig\Util\CallableParameters;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
@@ -43,7 +46,8 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
private $filters;
|
||||
/** @var array<string, int> */
|
||||
private $functions;
|
||||
private $needsToStringWrap = false;
|
||||
/** @var array<string, int> */
|
||||
private $tests;
|
||||
|
||||
public function enterNode(Node $node, Environment $env): Node
|
||||
{
|
||||
@@ -52,51 +56,64 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
$this->tags = [];
|
||||
$this->filters = [];
|
||||
$this->functions = [];
|
||||
|
||||
return $node;
|
||||
$this->tests = [];
|
||||
} elseif ($this->inAModule) {
|
||||
// look for tags
|
||||
if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) {
|
||||
if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()]) && !$this->isTagAlwaysAllowedInSandbox($env, $node->getNodeTag())) {
|
||||
$this->tags[$node->getNodeTag()] = $node->getTemplateLine();
|
||||
}
|
||||
|
||||
// look for filters
|
||||
if ($node instanceof FilterExpression && !isset($this->filters[$node->getAttribute('name')])) {
|
||||
$this->filters[$node->getAttribute('name')] = $node->getTemplateLine();
|
||||
if ($node instanceof FilterExpression && !isset($this->filters[$name = $node->getAttribute('name')]) && !$this->isFilterAlwaysAllowedInSandbox($env, $node)) {
|
||||
$this->filters[$name] = $node->getTemplateLine();
|
||||
}
|
||||
|
||||
// look for functions
|
||||
if ($node instanceof FunctionExpression && !isset($this->functions[$node->getAttribute('name')])) {
|
||||
$this->functions[$node->getAttribute('name')] = $node->getTemplateLine();
|
||||
if ($node instanceof FunctionExpression && !isset($this->functions[$name = $node->getAttribute('name')]) && !$this->isFunctionAlwaysAllowedInSandbox($env, $node)) {
|
||||
$this->functions[$name] = $node->getTemplateLine();
|
||||
}
|
||||
|
||||
// look for tests
|
||||
if ($node instanceof TestExpression && !isset($this->tests[$name = $node->getAttribute('name')]) && !$this->isTestAlwaysAllowedInSandbox($env, $node)) {
|
||||
$this->tests[$name] = $node->getTemplateLine();
|
||||
}
|
||||
|
||||
// look for functions whose parser callable replaced the FunctionExpression
|
||||
// with a specialized node (e.g. `parent`, `block`, `attribute`); the
|
||||
// original function name was stashed by FunctionExpressionParser.
|
||||
if ($node->hasAttribute('sandboxed_function_name')) {
|
||||
$name = $node->getAttribute('sandboxed_function_name');
|
||||
if (!isset($this->functions[$name]) && !$this->isSandboxedFunctionAlwaysAllowedInSandbox($env, $node, $name)) {
|
||||
$this->functions[$name] = $node->getTemplateLine();
|
||||
}
|
||||
}
|
||||
|
||||
// the .. operator is equivalent to the range() function
|
||||
if ($node instanceof RangeBinary && !isset($this->functions['range'])) {
|
||||
if ($node instanceof RangeBinary && !isset($this->functions['range']) && !$this->isFunctionNameAlwaysAllowedInSandbox($env, 'range')) {
|
||||
$this->functions['range'] = $node->getTemplateLine();
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof PrintNode) {
|
||||
$this->needsToStringWrap = true;
|
||||
$this->wrapNode($node, 'expr');
|
||||
}
|
||||
// wrap children that the node itself will string-coerce at runtime;
|
||||
// applies to ModuleNode (`parent` slot for {% extends %}) too
|
||||
if ($this->inAModule && $node instanceof CoercesChildrenToStringInterface) {
|
||||
$params = CallableParameters::fromNode($node, $env);
|
||||
foreach ($node->getStringCoercedChildNames() as $childName) {
|
||||
// For Filter/Function/Test calls, consult the PHP callable
|
||||
// signature: skip wrapping arguments whose param type cannot
|
||||
// implicitly string-coerce (e.g. `int`, a `final` value object).
|
||||
if (null !== $params && 'arguments' === $childName) {
|
||||
$this->wrapArguments($node, $params);
|
||||
|
||||
if ($node instanceof SetNode && !$node->getAttribute('capture')) {
|
||||
$this->needsToStringWrap = true;
|
||||
}
|
||||
|
||||
// wrap outer nodes that can implicitly call __toString()
|
||||
if ($this->needsToStringWrap) {
|
||||
if ($node instanceof ConcatBinary) {
|
||||
$this->wrapNode($node, 'left');
|
||||
$this->wrapNode($node, 'right');
|
||||
continue;
|
||||
}
|
||||
if ($node instanceof FilterExpression) {
|
||||
$this->wrapNode($node, 'node');
|
||||
$this->wrapArrayNode($node, 'arguments');
|
||||
}
|
||||
if ($node instanceof FunctionExpression) {
|
||||
$this->wrapArrayNode($node, 'arguments');
|
||||
if (null !== $params && 'node' === $childName && $node instanceof FilterExpression) {
|
||||
// The filter's input value maps to the first PHP parameter.
|
||||
if (isset($params[0]) && CallableParameters::isStringCoercionSafe($params[0]->getType(), $params[0]->getDeclaringClass())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$this->wrapNode($node, $childName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,36 +126,166 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
$this->inAModule = false;
|
||||
|
||||
$node->setNode('constructor_end', new Nodes([new CheckSecurityCallNode(), $node->getNode('constructor_end')]));
|
||||
$node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')]));
|
||||
} elseif ($this->inAModule) {
|
||||
if ($node instanceof PrintNode || $node instanceof SetNode) {
|
||||
$this->needsToStringWrap = false;
|
||||
}
|
||||
$node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions, $this->tests), $node->getNode('class_end')]));
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function wrapNode(Node $node, string $name): void
|
||||
/**
|
||||
* Wraps each entry in the `arguments` slot only when the corresponding
|
||||
* PHP parameter type can implicitly string-coerce.
|
||||
*
|
||||
* @param list<\ReflectionParameter> $params parameters relative to the
|
||||
* first template argument (for
|
||||
* filters and tests: starting
|
||||
* after the `node`/input
|
||||
* parameter)
|
||||
*/
|
||||
private function wrapArguments(Node $node, array $params): void
|
||||
{
|
||||
$expr = $node->getNode($name);
|
||||
if (($expr instanceof ContextVariable || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) {
|
||||
$node->setNode($name, new CheckToStringNode($expr));
|
||||
} elseif ($expr instanceof SpreadUnary) {
|
||||
$this->wrapNode($expr, 'node');
|
||||
} elseif ($expr instanceof ArrayExpression) {
|
||||
foreach ($expr as $name => $_) {
|
||||
$this->wrapNode($expr, $name);
|
||||
$arguments = $node->getNode('arguments');
|
||||
if (!$arguments instanceof Nodes && !$arguments instanceof ArrayExpression) {
|
||||
$this->wrapNode($node, 'arguments');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Filters and tests pass their input value (`node`) as the first PHP
|
||||
// param, so their template arguments start at offset 1.
|
||||
$positional = \array_slice($params, $node->hasNode('node') ? 1 : 0);
|
||||
$variadic = null;
|
||||
$byName = [];
|
||||
foreach ($positional as $p) {
|
||||
if ($p->isVariadic()) {
|
||||
$variadic = $p;
|
||||
break;
|
||||
}
|
||||
$byName[$this->normalizeName($p->getName())] ??= $p;
|
||||
}
|
||||
|
||||
$positionalIdx = 0;
|
||||
foreach ($arguments as $key => $_) {
|
||||
if (\is_int($key)) {
|
||||
$param = $positional[$positionalIdx] ?? $variadic;
|
||||
if (null !== $param && !$param->isVariadic()) {
|
||||
++$positionalIdx;
|
||||
}
|
||||
} else {
|
||||
$param = $byName[$this->normalizeName($key)] ?? $variadic;
|
||||
}
|
||||
|
||||
if (null !== $param && CallableParameters::isStringCoercionSafe($param->getType(), $param->getDeclaringClass())) {
|
||||
continue;
|
||||
}
|
||||
$this->wrapNode($arguments, (string) $key);
|
||||
}
|
||||
}
|
||||
|
||||
private function wrapArrayNode(Node $node, string $name): void
|
||||
private function normalizeName(string $name): string
|
||||
{
|
||||
$args = $node->getNode($name);
|
||||
foreach ($args as $name => $_) {
|
||||
$this->wrapNode($args, $name);
|
||||
return strtolower(str_replace('_', '', $name));
|
||||
}
|
||||
|
||||
private function wrapNode(Node $node, string $name): void
|
||||
{
|
||||
$expr = $node->getNode($name);
|
||||
// `_self` is internal: it compiles to `$this->getTemplateName()` and is always a string
|
||||
if ($expr instanceof ContextVariable && '_self' === $expr->getAttribute('name')) {
|
||||
return;
|
||||
}
|
||||
if (($expr instanceof ContextVariable || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) {
|
||||
$node->setNode($name, new CheckToStringNode($expr));
|
||||
} elseif ($expr instanceof SpreadUnary) {
|
||||
$expr->setNode('node', new CheckToStringNode($expr->getNode('node'), true));
|
||||
} elseif ($expr instanceof ArrayExpression || $expr instanceof Nodes) {
|
||||
foreach ($expr as $name => $_) {
|
||||
$this->wrapNode($expr, $name);
|
||||
}
|
||||
} elseif ($expr instanceof OperatorEscapeInterface) {
|
||||
foreach ($expr->getOperandNamesToEscape() as $operandName) {
|
||||
$this->wrapNode($expr, $operandName);
|
||||
}
|
||||
} elseif ($expr instanceof FilterExpression || $expr instanceof FunctionExpression) {
|
||||
$node->setNode($name, new CheckToStringNode($expr));
|
||||
}
|
||||
}
|
||||
|
||||
private function isTagAlwaysAllowedInSandbox(Environment $env, string $name): bool
|
||||
{
|
||||
if (null === $parser = $env->getTokenParser($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isAlwaysAllowedInSandbox($parser);
|
||||
}
|
||||
|
||||
private function isFilterAlwaysAllowedInSandbox(Environment $env, FilterExpression $node): bool
|
||||
{
|
||||
if ($node->hasAttribute('twig_callable')) {
|
||||
$filter = $node->getAttribute('twig_callable');
|
||||
} elseif (null === $filter = $env->getFilter($node->getAttribute('name'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isAlwaysAllowedInSandbox($filter);
|
||||
}
|
||||
|
||||
private function isFunctionAlwaysAllowedInSandbox(Environment $env, FunctionExpression $node): bool
|
||||
{
|
||||
if ($node->hasAttribute('twig_callable')) {
|
||||
$function = $node->getAttribute('twig_callable');
|
||||
} elseif (null === $function = $env->getFunction($node->getAttribute('name'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isAlwaysAllowedInSandbox($function);
|
||||
}
|
||||
|
||||
private function isTestAlwaysAllowedInSandbox(Environment $env, TestExpression $node): bool
|
||||
{
|
||||
if ($node->hasAttribute('twig_callable')) {
|
||||
$test = $node->getAttribute('twig_callable');
|
||||
} elseif (null === $test = $env->getTest($node->getAttribute('name'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isAlwaysAllowedInSandbox($test);
|
||||
}
|
||||
|
||||
private function isSandboxedFunctionAlwaysAllowedInSandbox(Environment $env, Node $node, string $name): bool
|
||||
{
|
||||
if ($node->hasAttribute('sandboxed_function')) {
|
||||
$function = $node->getAttribute('sandboxed_function');
|
||||
} elseif (null === $function = $env->getFunction($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isAlwaysAllowedInSandbox($function);
|
||||
}
|
||||
|
||||
private function isFunctionNameAlwaysAllowedInSandbox(Environment $env, string $name): bool
|
||||
{
|
||||
if (null === $function = $env->getFunction($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isAlwaysAllowedInSandbox($function);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TwigCallableInterface|TokenParserInterface $subject
|
||||
*/
|
||||
private static function isAlwaysAllowedInSandbox($subject): bool
|
||||
{
|
||||
if (method_exists($subject, 'isAlwaysAllowedInSandbox')) {
|
||||
return $subject->isAlwaysAllowedInSandbox();
|
||||
}
|
||||
|
||||
$interface = $subject instanceof TokenParserInterface ? TokenParserInterface::class : TwigCallableInterface::class;
|
||||
trigger_deprecation('twig/twig', '3.28', 'Not implementing the "isAlwaysAllowedInSandbox()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $subject::class, $interface);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
|
||||
@@ -29,8 +29,6 @@ use Twig\Node\Expression\Variable\TemplateVariable;
|
||||
use Twig\Node\MacroNode;
|
||||
use Twig\Node\ModuleNode;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\NodeCaptureInterface;
|
||||
use Twig\Node\NodeOutputInterface;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Node\PrintNode;
|
||||
use Twig\Node\TextNode;
|
||||
@@ -54,6 +52,7 @@ class Parser
|
||||
private $importedSymbols;
|
||||
private $traits;
|
||||
private $embeddedTemplates = [];
|
||||
private int $lastEmbedIndex = 0;
|
||||
private $varNameSalt = 0;
|
||||
private $ignoreUnknownTwigCallables = false;
|
||||
private ExpressionParsers $parsers;
|
||||
@@ -81,8 +80,13 @@ class Parser
|
||||
*/
|
||||
public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
|
||||
{
|
||||
// reset on root parse() calls only, so the counter spans nested/reentrant parses
|
||||
if (!$this->stack) {
|
||||
$this->lastEmbedIndex = 0;
|
||||
}
|
||||
|
||||
$vars = get_object_vars($this);
|
||||
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['varNameSalt']);
|
||||
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['lastEmbedIndex'], $vars['varNameSalt']);
|
||||
$this->stack[] = $vars;
|
||||
|
||||
// node visitors
|
||||
@@ -102,10 +106,6 @@ class Parser
|
||||
|
||||
try {
|
||||
$body = $this->subparse($test, $dropNeedle);
|
||||
|
||||
if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
|
||||
$body = new EmptyNode();
|
||||
}
|
||||
} catch (SyntaxError $e) {
|
||||
if (!$e->getSourceContext()) {
|
||||
$e->setSourceContext($this->stream->getSourceContext());
|
||||
@@ -120,6 +120,10 @@ class Parser
|
||||
$this->expressionRefs = null;
|
||||
}
|
||||
|
||||
if ($this->parent) {
|
||||
$body = $this->cleanupBodyForChildTemplates($body);
|
||||
}
|
||||
|
||||
$node = new ModuleNode(
|
||||
new BodyNode([$body]),
|
||||
$this->parent,
|
||||
@@ -319,7 +323,7 @@ class Parser
|
||||
*/
|
||||
public function embedTemplate(ModuleNode $template)
|
||||
{
|
||||
$template->setIndex(mt_rand());
|
||||
$template->setIndex(++$this->lastEmbedIndex);
|
||||
|
||||
$this->embeddedTemplates[] = $template;
|
||||
}
|
||||
@@ -410,13 +414,21 @@ class Parser
|
||||
return $this->parent || 0 < \count($this->traits);
|
||||
}
|
||||
|
||||
public function setParent(?Node $parent): void
|
||||
public function setParent(?Node $parent, bool $throwOnMultiple = true): void
|
||||
{
|
||||
if (null === $parent) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__);
|
||||
}
|
||||
|
||||
if (null !== $parent && !$parent instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" is deprecated, pass an "AbstractExpression" instance instead.', $parent::class, __METHOD__);
|
||||
}
|
||||
|
||||
if (null !== $this->parent) {
|
||||
if (!$throwOnMultiple) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext());
|
||||
}
|
||||
|
||||
@@ -447,7 +459,7 @@ class Parser
|
||||
|
||||
if (!$function) {
|
||||
if ($this->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigFunction($name, fn () => '');
|
||||
return new TwigFunction($name, static fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFunctions()));
|
||||
@@ -476,7 +488,7 @@ class Parser
|
||||
}
|
||||
if (!$filter) {
|
||||
if ($this->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigFilter($name, fn () => '');
|
||||
return new TwigFilter($name, static fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFilters()));
|
||||
@@ -524,7 +536,7 @@ class Parser
|
||||
|
||||
if (!$test) {
|
||||
if ($this->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigTest($name, fn () => '');
|
||||
return new TwigTest($name, static fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $this->stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getTests()));
|
||||
@@ -540,52 +552,23 @@ class Parser
|
||||
return $test;
|
||||
}
|
||||
|
||||
private function filterBodyNodes(Node $node, bool $nested = false): ?Node
|
||||
private function cleanupBodyForChildTemplates(Node $body): Node
|
||||
{
|
||||
// check that the body does not contain non-empty output nodes
|
||||
if (
|
||||
($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
|
||||
|| (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface)
|
||||
) {
|
||||
if (str_contains((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) {
|
||||
$t = substr($node->getAttribute('data'), 3);
|
||||
if ('' === $t || ctype_space($t)) {
|
||||
// bypass empty nodes starting with a BOM
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext());
|
||||
if ($body instanceof BlockReferenceNode || ($body instanceof TextNode && $body->isBlank())) {
|
||||
return new EmptyNode();
|
||||
}
|
||||
|
||||
// bypass nodes that "capture" the output
|
||||
if ($node instanceof NodeCaptureInterface) {
|
||||
// a "block" tag in such a node will serve as a block definition AND be displayed in place as well
|
||||
return $node;
|
||||
}
|
||||
|
||||
// "block" tags that are not captured (see above) are only used for defining
|
||||
// the content of the block. In such a case, nesting it does not work as
|
||||
// expected as the definition is not part of the default template code flow.
|
||||
if ($nested && $node instanceof BlockReferenceNode) {
|
||||
throw new SyntaxError('A block definition cannot be nested under non-capturing nodes.', $node->getTemplateLine(), $this->stream->getSourceContext());
|
||||
}
|
||||
|
||||
if ($node instanceof NodeOutputInterface) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// here, $nested means "being at the root level of a child template"
|
||||
// we need to discard the wrapping "Node" for the "body" node
|
||||
// Node::class !== \get_class($node) should be removed in Twig 4.0
|
||||
$nested = $nested || (Node::class !== $node::class && !$node instanceof Nodes);
|
||||
foreach ($node as $k => $n) {
|
||||
if (null !== $n && null === $this->filterBodyNodes($n, $nested)) {
|
||||
$node->removeNode($k);
|
||||
foreach ($body as $k => $node) {
|
||||
if ($node instanceof BlockReferenceNode) {
|
||||
// as it has a parent, the block reference won't be used
|
||||
$body->removeNode($k);
|
||||
} elseif ($node instanceof TextNode && $node->isBlank()) {
|
||||
// remove nodes considered as "empty"
|
||||
$body->removeNode($k);
|
||||
}
|
||||
}
|
||||
|
||||
return $node;
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function checkPrecedenceDeprecations(ExpressionParserInterface $expressionParser, AbstractExpression $expr)
|
||||
|
||||
@@ -31,11 +31,16 @@ abstract class BaseDumper
|
||||
|
||||
abstract protected function formatTime(Profile $profile, $percent): string;
|
||||
|
||||
protected function formatRoot(Profile $profile): string
|
||||
{
|
||||
return $profile->getName();
|
||||
}
|
||||
|
||||
private function dumpProfile(Profile $profile, $prefix = '', $sibling = false): string
|
||||
{
|
||||
if ($profile->isRoot()) {
|
||||
$this->root = $profile->getDuration();
|
||||
$start = $profile->getName();
|
||||
$start = $this->formatRoot($profile);
|
||||
} else {
|
||||
if ($profile->isTemplate()) {
|
||||
$start = $this->formatTemplate($profile, $prefix);
|
||||
|
||||
@@ -30,18 +30,28 @@ final class HtmlDumper extends BaseDumper
|
||||
return '<pre>'.parent::dump($profile).'</pre>';
|
||||
}
|
||||
|
||||
protected function formatRoot(Profile $profile): string
|
||||
{
|
||||
return self::escape($profile->getName());
|
||||
}
|
||||
|
||||
protected function formatTemplate(Profile $profile, $prefix): string
|
||||
{
|
||||
return \sprintf('%s└ <span style="background-color: %s">%s</span>', $prefix, self::$colors['template'], $profile->getTemplate());
|
||||
return \sprintf('%s└ <span style="background-color: %s">%s</span>', $prefix, self::$colors['template'], self::escape($profile->getTemplate()));
|
||||
}
|
||||
|
||||
protected function formatNonTemplate(Profile $profile, $prefix): string
|
||||
{
|
||||
return \sprintf('%s└ %s::%s(<span style="background-color: %s">%s</span>)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName());
|
||||
return \sprintf('%s└ %s::%s(<span style="background-color: %s">%s</span>)', $prefix, self::escape($profile->getTemplate()), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', self::escape($profile->getName()));
|
||||
}
|
||||
|
||||
protected function formatTime(Profile $profile, $percent): string
|
||||
{
|
||||
return \sprintf('<span style="color: %s">%.2fms/%.0f%%</span>', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent);
|
||||
}
|
||||
|
||||
private static function escape(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ final class Profile implements \IteratorAggregate, \Serializable
|
||||
|
||||
public function unserialize($data): void
|
||||
{
|
||||
$this->__unserialize(unserialize($data));
|
||||
$this->__unserialize(unserialize($data, ['allowed_classes' => [self::class]]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Source;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -237,7 +240,7 @@ function twig_sort_filter(Environment $env, $array, $arrow = null)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
return CoreExtension::sort($env, $array, $arrow);
|
||||
return CoreExtension::sort($env, twig_resolve_is_sandboxed($env), $array, $arrow);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -461,11 +464,11 @@ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
|
||||
*
|
||||
* @deprecated since Twig 3.9
|
||||
*/
|
||||
function twig_array_column($array, $name, $index = null): array
|
||||
function twig_array_column(Environment $env, $array, $name, $index = null): array
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
return CoreExtension::column($array, $name, $index);
|
||||
return CoreExtension::column($env, twig_resolve_is_sandboxed($env), $array, $name, $index);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -477,7 +480,7 @@ function twig_array_filter(Environment $env, $array, $arrow)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
return CoreExtension::filter($env, $array, $arrow);
|
||||
return CoreExtension::filter($env, twig_resolve_is_sandboxed($env), $array, $arrow);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,7 +492,7 @@ function twig_array_map(Environment $env, $array, $arrow)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
return CoreExtension::map($env, $array, $arrow);
|
||||
return CoreExtension::map($env, twig_resolve_is_sandboxed($env), $array, $arrow);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,7 +504,7 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
return CoreExtension::reduce($env, $array, $arrow, $initial);
|
||||
return CoreExtension::reduce($env, twig_resolve_is_sandboxed($env), $array, $arrow, $initial);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,7 +516,7 @@ function twig_array_some(Environment $env, $array, $arrow)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
return CoreExtension::arraySome($env, $array, $arrow);
|
||||
return CoreExtension::arraySome($env, $array, $arrow, twig_resolve_is_sandboxed($env));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,7 +528,7 @@ function twig_array_every(Environment $env, $array, $arrow)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
return CoreExtension::arrayEvery($env, $array, $arrow);
|
||||
return CoreExtension::arrayEvery($env, $array, $arrow, twig_resolve_is_sandboxed($env));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,5 +540,33 @@ function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
|
||||
|
||||
CoreExtension::checkArrow($env, $arrow, $thing, $type);
|
||||
CoreExtension::checkArrow(twig_resolve_is_sandboxed($env), $arrow, $thing, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovers the calling Template's Source by walking the PHP backtrace.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function twig_resolve_caller_source(): ?Source
|
||||
{
|
||||
foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) {
|
||||
if (isset($trace['object']) && $trace['object'] instanceof Template) {
|
||||
return $trace['object']->getSourceContext();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function twig_resolve_is_sandboxed(Environment $env): bool
|
||||
{
|
||||
if (!$env->hasExtension(SandboxExtension::class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $env->getExtension(SandboxExtension::class)->isSandboxed(twig_resolve_caller_source());
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
}
|
||||
|
||||
$string = (string) $string;
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) {
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'html_attr_relaxed', 'url'], true)) {
|
||||
// we return the input as is (which can be of any type)
|
||||
return $string;
|
||||
}
|
||||
@@ -191,7 +191,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
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) {
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', static function ($matches) {
|
||||
$char = $matches[0];
|
||||
|
||||
/*
|
||||
@@ -243,7 +243,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
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) {
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', static function ($matches) {
|
||||
$char = $matches[0];
|
||||
|
||||
return \sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
|
||||
@@ -256,6 +256,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $string;
|
||||
|
||||
case 'html_attr':
|
||||
case 'html_attr_relaxed':
|
||||
if ('UTF-8' !== $charset) {
|
||||
$string = $this->convertEncoding($string, 'UTF-8', $charset);
|
||||
}
|
||||
@@ -264,7 +265,12 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
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) {
|
||||
$regex = match ($strategy) {
|
||||
'html_attr' => '#[^a-zA-Z0-9,\.\-_]#Su',
|
||||
'html_attr_relaxed' => '#[^a-zA-Z0-9,\.\-_:@\[\]]#Su',
|
||||
};
|
||||
|
||||
$string = preg_replace_callback($regex, static function ($matches) {
|
||||
/**
|
||||
* This function is adapted from code coming from Zend Framework.
|
||||
*
|
||||
@@ -323,7 +329,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $this->escapers[$strategy]($string, $charset);
|
||||
}
|
||||
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers)));
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr', 'html_attr_relaxed'], array_keys($this->escapers)));
|
||||
|
||||
throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies));
|
||||
}
|
||||
|
||||
33
lib/twig/twig/src/Sandbox/SecurityNotAllowedTestError.php
Normal file
33
lib/twig/twig/src/Sandbox/SecurityNotAllowedTestError.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Sandbox;
|
||||
|
||||
/**
|
||||
* Exception thrown when a not allowed test is used in a template.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class SecurityNotAllowedTestError extends SecurityError
|
||||
{
|
||||
private string $testName;
|
||||
|
||||
public function __construct(string $message, string $testName)
|
||||
{
|
||||
parent::__construct($message);
|
||||
$this->testName = $testName;
|
||||
}
|
||||
|
||||
public function getTestName(): string
|
||||
{
|
||||
return $this->testName;
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,17 @@ final class SecurityPolicy implements SecurityPolicyInterface
|
||||
private $allowedMethods;
|
||||
private $allowedProperties;
|
||||
private $allowedFunctions;
|
||||
private array $allowedTests;
|
||||
private bool $strict = false;
|
||||
|
||||
public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [])
|
||||
public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [], array $allowedTests = [])
|
||||
{
|
||||
$this->allowedTags = $allowedTags;
|
||||
$this->allowedFilters = $allowedFilters;
|
||||
$this->setAllowedMethods($allowedMethods);
|
||||
$this->allowedProperties = $allowedProperties;
|
||||
$this->allowedFunctions = $allowedFunctions;
|
||||
$this->allowedTests = $allowedTests;
|
||||
}
|
||||
|
||||
public function setAllowedTags(array $tags): void
|
||||
@@ -64,14 +67,38 @@ final class SecurityPolicy implements SecurityPolicyInterface
|
||||
$this->allowedFunctions = $functions;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
public function setAllowedTests(array $tests): void
|
||||
{
|
||||
$this->allowedTests = $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles strict mode.
|
||||
*
|
||||
* In strict mode, the tags, functions, and tests that are historically always
|
||||
* allowed in a sandbox (the ``extends`` and ``use`` tags, the ``parent``,
|
||||
* ``block``, and ``attribute`` functions, and any test) are no longer implicitly
|
||||
* allowed and must be added to the relevant allow-list to be usable. Use this
|
||||
* flag in 3.x to opt-in to the forthcoming 4.0 behavior and silence the related
|
||||
* deprecations.
|
||||
*/
|
||||
public function setStrict(bool $strict): void
|
||||
{
|
||||
$this->strict = $strict;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions, array $tests = []): void
|
||||
{
|
||||
if (\func_num_args() < 4) {
|
||||
trigger_deprecation('twig/twig', '3.28', 'Not passing the "$tests" argument to "%s::checkSecurity()" is deprecated; it will be required in 4.0.', static::class);
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (!\in_array($tag, $this->allowedTags, true)) {
|
||||
if ('extends' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
|
||||
} elseif ('use' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
|
||||
if (!$this->strict && 'extends' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} elseif (!$this->strict && 'use' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} else {
|
||||
throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
|
||||
}
|
||||
@@ -86,7 +113,25 @@ final class SecurityPolicy implements SecurityPolicyInterface
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (!\in_array($function, $this->allowedFunctions, true)) {
|
||||
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
if (!$this->strict && 'parent' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "parent" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} elseif (!$this->strict && 'block' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "block" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} elseif (!$this->strict && 'attribute' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "attribute" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} else {
|
||||
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tests as $test) {
|
||||
if (!\in_array($test, $this->allowedTests, true)) {
|
||||
if (!$this->strict) {
|
||||
trigger_deprecation('twig/twig', '3.28', 'The "%s" test is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).', $test);
|
||||
} else {
|
||||
throw new SecurityNotAllowedTestError(\sprintf('Test "%s" is not allowed.', $test), $test);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,11 @@ interface SecurityPolicyInterface
|
||||
* @param string[] $tags
|
||||
* @param string[] $filters
|
||||
* @param string[] $functions
|
||||
* @param string[] $tests
|
||||
*
|
||||
* @throws SecurityError
|
||||
*/
|
||||
public function checkSecurity($tags, $filters, $functions): void;
|
||||
public function checkSecurity($tags, $filters, $functions/* , array $tests */): void;
|
||||
|
||||
/**
|
||||
* @param object $obj
|
||||
|
||||
@@ -17,6 +17,8 @@ use Twig\Source;
|
||||
* Interface for a class that can optionally enable the sandbox mode based on a template's Twig\Source.
|
||||
*
|
||||
* @author Yaakov Saxon
|
||||
*
|
||||
* @deprecated since Twig 3.27.0 with no replacement
|
||||
*/
|
||||
interface SourcePolicyInterface
|
||||
{
|
||||
|
||||
@@ -44,4 +44,23 @@ final class Source
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 1-based column for a 0-based byte offset in the source code.
|
||||
*
|
||||
* A negative offset means the position is unknown and yields null.
|
||||
*
|
||||
* @return positive-int|null
|
||||
*/
|
||||
public function getColumn(int $offset): ?int
|
||||
{
|
||||
if ($offset < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$before = str_replace(["\r\n", "\r"], "\n", substr($this->code, 0, $offset));
|
||||
$lineStart = strrpos($before, "\n");
|
||||
|
||||
return false === $lineStart ? \strlen($before) + 1 : \strlen($before) - $lineStart;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,14 @@ abstract class Template
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
// The compiled doGetParent() may evaluate user expressions (filters,
|
||||
// functions, method calls) when the parent name is dynamic. Make sure
|
||||
// the sandbox security check runs first so those expressions cannot
|
||||
// bypass the allow-list when getParent() is reached before the first
|
||||
// ensureSecurityChecked() call on this template (e.g. via
|
||||
// getTemplateForMacro() or yieldBlock() into a pre-warmed instance).
|
||||
$this->ensureSecurityChecked();
|
||||
|
||||
if (!$parent = $this->doGetParent($context)) {
|
||||
return false;
|
||||
}
|
||||
@@ -158,7 +166,7 @@ abstract class Template
|
||||
if ($this->env->isDebug()) {
|
||||
ob_start();
|
||||
} else {
|
||||
ob_start(function () { return ''; });
|
||||
ob_start(static function () { return ''; });
|
||||
}
|
||||
$this->displayParentBlock($name, $context, $blocks);
|
||||
|
||||
@@ -193,7 +201,7 @@ abstract class Template
|
||||
if ($this->env->isDebug()) {
|
||||
ob_start();
|
||||
} else {
|
||||
ob_start(function () { return ''; });
|
||||
ob_start(static function () { return ''; });
|
||||
}
|
||||
try {
|
||||
$this->displayBlock($name, $context, $blocks, $useBlocks);
|
||||
@@ -367,7 +375,7 @@ abstract class Template
|
||||
if ($this->env->isDebug()) {
|
||||
ob_start();
|
||||
} else {
|
||||
ob_start(function () { return ''; });
|
||||
ob_start(static function () { return ''; });
|
||||
}
|
||||
try {
|
||||
$this->display($context);
|
||||
@@ -399,6 +407,7 @@ abstract class Template
|
||||
$blocks = array_merge($this->blocks, $blocks);
|
||||
|
||||
try {
|
||||
$this->ensureSecurityChecked();
|
||||
yield from $this->doDisplay($context, $blocks);
|
||||
} catch (Error $e) {
|
||||
if (!$e->getSourceContext()) {
|
||||
@@ -431,6 +440,8 @@ abstract class Template
|
||||
} elseif (isset($this->blocks[$name])) {
|
||||
$template = $this->blocks[$name][0];
|
||||
$block = $this->blocks[$name][1];
|
||||
// expose this template's own blocks so nested block() calls resolve against them when the block is rendered directly (e.g. block(name, template))
|
||||
$blocks = array_merge($this->blocks, $blocks);
|
||||
} else {
|
||||
$template = null;
|
||||
$block = null;
|
||||
@@ -443,6 +454,7 @@ abstract class Template
|
||||
|
||||
if (null !== $template) {
|
||||
try {
|
||||
$template->ensureSecurityChecked();
|
||||
yield from $template->$block($context, $blocks);
|
||||
} catch (Error $e) {
|
||||
if (!$e->getSourceContext()) {
|
||||
@@ -510,12 +522,16 @@ abstract class Template
|
||||
protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self
|
||||
{
|
||||
if (method_exists($this, $name)) {
|
||||
$this->ensureSecurityChecked();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$parent = $this;
|
||||
while ($parent = $parent->getParent($context)) {
|
||||
if (method_exists($parent, $name)) {
|
||||
$parent->ensureSecurityChecked();
|
||||
|
||||
return $parent;
|
||||
}
|
||||
}
|
||||
@@ -523,6 +539,15 @@ abstract class Template
|
||||
throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the sandbox security check against the current sandbox state.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function ensureSecurityChecked(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generated method to display the template with the given context.
|
||||
*
|
||||
|
||||
@@ -39,10 +39,14 @@ final class Token
|
||||
*/
|
||||
public const SPREAD_TYPE = 13;
|
||||
|
||||
/**
|
||||
* @param non-negative-int|null $offset
|
||||
*/
|
||||
public function __construct(
|
||||
private int $type,
|
||||
private $value,
|
||||
private int $lineno,
|
||||
private ?int $offset = null,
|
||||
) {
|
||||
if (self::ARROW_TYPE === $type) {
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE);
|
||||
@@ -124,6 +128,19 @@ final class Token
|
||||
return $this->lineno;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 0-based byte offset of the token in the source code.
|
||||
*
|
||||
* Returns null for tokens that are not tied to a source position (e.g.
|
||||
* tokens synthesized by a token parser).
|
||||
*
|
||||
* @return non-negative-int|null
|
||||
*/
|
||||
public function getOffset(): ?int
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Twig 3.19
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user