[Web] Update composer libraries

This commit is contained in:
FreddleSpl0it committed 2026-10-28 11:56:29 +01:00
1 parent 9cc8ac2908
commit bd8c784118
1297 files changed
+71585 -24460

No files matched your search

+314
View File
@@ -1,3 +1,306 @@
# 3.30.0 (2026-09-25)
* Fix `split`, `random`, and `shuffle` merging a trailing newline into the last character of a string
* Speed up splitting a string into characters in `split`, `random`, and `shuffle`
* Speed up escaping by fetching the escaper runtime once per template
* Speed up adding extensions to an environment
* Fix the escaping safe analysis retaining every compiled template node for the lifetime of the environment
* Speed up loading a template that the environment has already loaded
* Speed up rendering by compiling a cheaper generator guard into templates
* Speed up compiling filter, function, and test calls
* Fix `IntlExtension` letting the pattern derived from a date formatter prototype override an explicit locale
* Fix `IntlExtension` not honoring the locale of a date formatter prototype configured with no date and time styles
* Speed up macro calls
* Fix `TemplateWrapper::unwrap()` failing when called without arguments, which is now deprecated
* Add `TemplateWrapper::getDefaultEscapeStrategy()` to know the escaping strategy a template was compiled with
* Report a clear error when `random`, `reverse`, `shuffle`, and `split` receive a string that is not valid UTF-8
* Fix the deprecation about omitting parentheses when calling a macro being reported twice for the same call
* Add the macro name to the deprecation about omitting parentheses when calling a macro
* Deprecate cloning a `Twig\Environment` instance
* Fix array access with a `Stringable` key on subclasses of `ArrayObject` and `ArrayIterator`
# 3.29.0 (2026-09-18)
* Fix the PHP warning and cryptic error when a block or a macro rendered on its own uses macros imported in the template body
* Fix `{% cache %}` always missing in the Symfony bundle when `framework.cache.app` uses a natively tag aware adapter
* Fix the sandbox resolving `use` trait templates before checking that the `use` tag is allowed
* Fix `html_attr` dropping `style` declarations whose value is `0`, `0.0` or `'0'`
* Fix the `default` filter fallback emitting an undefined variable warning when it uses the null-safe operator
* Fix the `matches` operator silently treating PCRE execution errors as non-matches; it now throws a `RuntimeError`
* Fix `TemplateWrapper::streamBlock()`, `TemplateWrapper::hasBlock()`, and `TemplateWrapper::getBlockNames()` omitting environment globals
* Fix exceptions from dynamic parent expressions escaping without template context
* Add the `BlockChain` class to compose blocks from multiple templates without using template internals
* Fix `TemplateWrapper::hasBlock()` and `TemplateWrapper::getBlockNames()` losing the `extends` line when the parent template does not exist
* Fix an output buffer leak when a parent block rendered in an expression throws in non-yield mode
* Add the `HtmlExtension::htmlAttrValue()` method to resolve a single HTML attribute value the way the `html_attr` function renders it
* Fix `html_attr` JSON encoding a `Stringable` value in a `data-*` attribute instead of using its string representation
* Add documentation comments to attach metadata to nodes (experimental)
* Fix destructuring patterns containing no variables (empty patterns or sequences with only empty slots) triggering a PHP fatal error instead of a `SyntaxError`
* Fix object and mapping destructuring evaluating the right-hand expression more than once
* Fix sequence destructuring of iterators throwing a `TypeError`
* Fix `MissingExtensionSuggestor` not suggesting the `twig/*-extra` package to install for some `html-extra`, `intl-extra`, and `string-extra` filters and functions
* Add `TempestMarkdown` to use `tempest/markdown` as the `markdown_to_html` converter
* Add the `include_only` function to render a template without giving it access to the current context
* Add the `Twig\Sandbox\SandboxInterface` interface and `Twig\Sandbox\Sandbox` class to render untrusted templates through a dedicated, always-sandboxed environment crafted for it
* Reject `TemplateWrapper` instances created by another `Environment`
* Add the `Twig\Extension\SandboxBridgeExtension` to render sandboxed templates from trusted templates with an explicit output escaping strategy
* Extract the sandbox runtime enforcement into a new internal `Twig\Sandbox\SecurityChecker` class used by compiled templates and `CoreExtension`
* Mark `SandboxExtension` as internal, use `Twig\Sandbox\Sandbox` instead
* Deprecate the `sandboxed` argument of the `include` function, use `Twig\Sandbox\Sandbox` instead
* Deprecate `SandboxExtension::enableSandbox()`, `disableSandbox()`, and `isSandboxedGlobally()`
* Normalize destructuring variable AST nodes as assignment targets
* Fix `IntlExtension` ignoring explicit date/time formats and configured calendars when using a date formatter prototype
* Add a `format_list` filter to `IntlExtension` to format a list of strings using PHP 8.5's `IntlListFormatter`
* Fix array access with a `Stringable` key for `ArrayObject` and `ArrayIterator` while preserving object keys for `SplObjectStorage`
* Fix duplicated macro argument names triggering a PHP fatal error instead of a `SyntaxError`
* Deprecate defining a macro more than once in the same template
* Deprecate `TemplateVariable` and `AssignTemplateVariable`; use `MacroVariable` and `AssignMacroVariable` instead
* Deprecate calling or testing a macro with a name whose case differs from its definition; macro names will be case-sensitive in 4.0
* Deprecate omitting parentheses when calling a macro; it will throw a `SyntaxError` in 4.0
* Deprecate using parentheses when testing a macro with the `defined` test; it will throw a `SyntaxError` in 4.0
* Deprecate calling a macro without a value for an argument that has no default value; the argument will be required in 4.0
* Deprecate passing extra or unknown arguments to a macro that does not declare a variadic argument; it will throw in 4.0
* Add support for declaring an explicit variadic macro argument (`{% macro foo(a, ...rest) %}`)
* Compile macros as closures stored in a per-template registry instead of `macro_`-prefixed PHP methods
* Represent imported macro namespaces with a narrow capability object instead of template instances
* Mark `Twig\Node\MacroNode` as `@final`; it will be final in Twig 4.0
* Deprecate not passing a `MacrosNode` instance as the macros of a `ModuleNode` constructor
* Change `MacroReferenceExpression` to take the bare macro name instead of a `macro_`-prefixed method name
* Deprecate resolving a macro through a `macro_`-prefixed name; pass the bare macro name to `MacroReferenceExpression`
# 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)
* Add sequence, mapping, and object destructuring
* Add `?.` null-safe operator
* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
* Fix opcache preload warning for unlinked anonymous class
* Fix spread operator behavior
# 3.22.2 (2025-12-14)
* Fix "cycle" with non-countable ArrayAccess + Traversable objects
* Use "getShareDir" as an indicator of Symfony version in Symfony bundle
* Fix escaper compatibility with PHP 8.5
# 3.22.1 (2025-11-16)
* Add support for Symfony 8
# 3.22.0 (2025-10-29)
* Add support for two words test in guard tag
* Add `Environment::registerUndefinedTestCallback()`
* Fix compatibility with Symfony 8
* Fix accessing arrays with stringable objects as key
* Avoid errors when failing to guess the template info for an error
* Fix expression parser compatibility layer
* Fix compiling 'index' with repr (not string) in EmbedNode
* Update configuration keys + allow extra keys for CommonMark extensions
* Allow usage of other Markdown converters than CommonMark in LeagueMarkdown
# 3.21.1 (2025-05-03)
* Fix ExtensionSet usage of BinaryOperatorExpressionParser
# 3.21.0 (2025-05-02)
* Fix wrong array index
* Deprecate `Template::loadTemplate()`
* Fix testing and expression when it evaluates to an instance of `Markup`
* Add `ReturnPrimitiveTypeInterface` (and sub-interfaces for number, boolean, string, and array)
* Add `SupportDefinedTestInterface` for expression nodes supporting the `defined` test
* Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence
* Deprecate operator precedence outside of the [0, 512] range
* Introduce expression parser classes to describe operators and operands provided by extensions
instead of arrays (it comes with many deprecations that are documented in
the ``deprecated`` documentation chapter)
* Deprecate the `Twig\ExpressionParser`, and `Twig\OperatorPrecedenceChange` classes
* Add attributes `AsTwigFilter`, `AsTwigFunction`, and `AsTwigTest` to ease extension development
# 3.20.0 (2025-02-13)
* Fix support for ignoring syntax errors in an undefined handler in guard
* Add configuration for Commonmark
* Fix wrong array index
* Bump minimum PHP version to 8.1
* Add support for registering callbacks for undefined functions, filters or token parsers in the IntegrationTestCase
* Use correct line number for `ForElseNode`
* Fix timezone conversion on strings
# 3.19.0 (2025-01-28)
* Fix a security issue where escaping was missing when using `??`
* Deprecate `Token::getType()`, use `Token::test()` instead
* Add `Token::toEnglish()`
* Add `ForElseNode`
* Deprecate `Twig\ExpressionParser::parseOnlyArguments()` and
`Twig\ExpressionParser::parseArguments()` (use
`Twig\ExpressionParser::parseNamedArguments()` instead)
* Fix `constant()` behavior when used with `??`
* Add the `invoke` filter
* Make `{}` optional for the `types` tag
* Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes
* Ignore static properties when using the dot operator
# 3.18.0 (2024-12-29)
* Support for invoking closures
* Fix unary operator precedence change
* Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag
* Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()`)
# 3.17.1 (2024-12-12)
* Fix the null coalescing operator when the test returns null
* Fix the Elvis operator when used as '? :' instead of '?:'
# 3.17.0 (2024-12-10)
* Fix ArrayAccess with objects as keys
* Support underscores in number literals
* Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead)
# 3.16.0 (2024-11-29)
* Deprecate `InlinePrint`
* Fix having macro variables starting with an underscore
* Deprecate not passing a `Source` instance to `TokenStream`
* Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead
# 3.15.0 (2024-11-17)
* [BC BREAK] Add support for accessing class constants with the dot operator;
this can be a BC break if you don't use UPPERCASE constant names
* Add Spanish inflector support for the `plural` and `singular` filters in the String extension
* Deprecate `TempNameExpression` in favor of `LocalVariable`
* Deprecate `NameExpression` in favor of `ContextVariable`
* Deprecate `AssignNameExpression` in favor of `AssignContextVariable`
* Remove `MacroAutoImportNodeVisitor`
* Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression`
* Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros
* Fix support for the "is defined" test on inherited macros
* Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`)
* Add named arguments support for macros
* Add a new `guard` tag that allows to test if some Twig callables are available at compilation time
* Allow arrow functions everywhere
* Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`)
* Add support for triggering deprecations for future operator precedence changes
* Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence
* Deprecate using the `??` binary operator without explicit parentheses
* Deprecate using the `~` binary operator in an expression with `+` or `-` without using parentheses to clarify precedence
* Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression`
* Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`)
* Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead.
* Add support for inline comments
* Add `Profile::getStartTime()` and `Profile::getEndTime()`
* Fix "ignore missing" when used on an "embed" tag
* Fix the possibility to override an aliased block (via use)
* Add template cache hot reload
* Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature
They were automatically converted to snake-cased before
* Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead
* Add support for argument unpackaging
* Add JSON support for the file extension escaping strategy
* Support Markup instances (and any other \Stringable) as dynamic mapping keys
* Deprecate the `sandbox` tag
* Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options)
* Add the `enum` function
* Add support for logical `xor` operator
# 3.14.2 (2024-11-07)
* Fix an infinite recursion in the sandbox code
# 3.14.1 (2024-11-06)
* [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects
They are now checked via the property policy
* Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()`
under some circumstances on an object even if the `__toString()` method is not allowed by the security policy
# 3.14.0 (2024-09-09)
* Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
@@ -44,6 +347,17 @@
* Fix integration tests when a test has more than one data/expect section and deprecations
* Add the `enum_cases` function
# 3.11.2 (2024-11-06)
* [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects
They are now checked via the property policy
* Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()`
under some circumstances on an object even if the `__toString()` method is not allowed by the security policy
# 3.11.1 (2024-09-10)
* Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
# 3.11.0 (2024-08-08)
* Deprecate `OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER`
+5 -4
View File
@@ -24,15 +24,16 @@
}
],
"require": {
"php": ">=8.0.2",
"php": ">=8.1.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php81": "^1.29"
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
"psr/container": "^1.0|^2.0"
"psr/container": "^1.0|^2.0",
"phpstan/phpstan": "^2.0@stable",
"php-cs-fixer/shim": "^3.0@stable"
},
"autoload": {
"files": [
@@ -32,11 +32,38 @@ 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' => '',
'alternative' => null,
], $options);
if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) {
throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class));
}
if ($this->options['deprecated']) {
if ($this->options['deprecation_info']) {
throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.');
}
trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.');
$this->options['deprecation_info'] = new DeprecatedCallableInfo(
$this->options['deprecating_package'],
$this->options['deprecated'],
null,
$this->options['alternative'],
);
}
if ($this->options['deprecation_info']) {
$this->options['deprecation_info']->setName($name);
$this->options['deprecation_info']->setType($this->getType());
}
}
public function __toString(): string
@@ -54,6 +81,9 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
return $this->dynamicName;
}
/**
* @return callable|array{class-string, string}|null
*/
public function getCallable()
{
return $this->callable;
@@ -79,6 +109,19 @@ 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
*/
public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self
{
$new = clone $this;
@@ -111,26 +154,46 @@ abstract class AbstractTwigCallable implements TwigCallableInterface
public function isDeprecated(): bool
{
return (bool) $this->options['deprecated'];
return (bool) $this->options['deprecation_info'];
}
public function triggerDeprecation(?string $file = null, ?int $line = null): void
{
$this->options['deprecation_info']->triggerDeprecation($file, $line);
}
/**
* @deprecated since Twig 3.15
*/
public function getDeprecatingPackage(): string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return $this->options['deprecating_package'];
}
/**
* @deprecated since Twig 3.15
*/
public function getDeprecatedVersion(): string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
}
/**
* @deprecated since Twig 3.15
*/
public function getAlternative(): ?string
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class);
return $this->options['alternative'];
}
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);
}
}
@@ -0,0 +1,60 @@
<?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\Attribute;
use Twig\DeprecatedCallableInfo;
use Twig\TwigFilter;
/**
* Registers a method as template filter.
*
* If the first argument of the method has Twig\Environment type-hint, the filter will receive the current environment.
* Additional arguments of the method come from the filter call.
*
* #[AsTwigFilter(name: 'foo')]
* function fooFilter(Environment $env, $string, $arg1 = null, ...) { ... }
*
* {{ 'string'|foo(arg1) }}
*
* @see TwigFilter
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
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 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,
) {
}
}
@@ -0,0 +1,56 @@
<?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\Attribute;
use Twig\DeprecatedCallableInfo;
use Twig\TwigFunction;
/**
* Registers a method as template function.
*
* If the first argument of the method has Twig\Environment type-hint, the function will receive the current environment.
* Additional arguments of the method come from the function call.
*
* #[AsTwigFunction(name: 'foo')]
* function fooFunction(Environment $env, string $string, $arg1 = null, ...) { ... }
*
* {{ foo('string', arg1) }}
*
* @see TwigFunction
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
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 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,
) {
}
}
@@ -0,0 +1,52 @@
<?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\Attribute;
use Twig\DeprecatedCallableInfo;
use Twig\TwigTest;
/**
* Registers a method as template test.
*
* The first argument is the value to test and the other arguments are the
* arguments passed to the test in the template.
*
* #[AsTwigTest(name: 'foo')]
* public function fooTest($value, $arg1 = null) { ... }
*
* {% if value is foo(arg1) %}
*
* @see TwigTest
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
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 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,
) {
}
}
+228
View File
@@ -0,0 +1,228 @@
<?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;
use Twig\Error\RuntimeError;
/**
* Composes blocks from several templates without composing their bodies or macros.
*/
final class BlockChain
{
/** @var list<Template|self> */
private array $templates = [];
/** @var list<Template|self> */
private array $lineage = [];
/** @var array<string, array{Template, string}> */
private array $blocks = [];
/**
* Whether the lineage can no longer move, making every further resolution pointless.
*/
private bool $fixed = false;
/**
* @param iterable<string|TemplateWrapper|self> $templates Templates and nested chains ordered from highest to lowest precedence
* @param array<string, mixed> $context Default variables used to resolve dynamic parent expressions
*/
public function __construct(
private Environment $env,
iterable $templates,
private array $context = [],
) {
foreach ($templates as $template) {
if ($template instanceof self) {
if ($env !== $template->env) {
throw new \LogicException('A block chain cannot contain a block chain from a different Twig environment.');
}
$this->templates[] = $template;
continue;
}
if (\is_string($template)) {
$template = $env->load($template);
}
if (!$template instanceof TemplateWrapper) {
throw new \TypeError(\sprintf('Block chain templates must be strings, "%s" or "%s" instances, "%s" given.', TemplateWrapper::class, self::class, get_debug_type($template)));
}
if (!$template->isOwnedBy($env)) {
throw new \LogicException('A block chain cannot contain templates from different Twig environments.');
}
$this->templates[] = $template->unwrap($env);
}
if (!$this->templates) {
throw new \InvalidArgumentException('A block chain requires at least one template.');
}
}
/**
* @param array<string, mixed> $context
*/
public function hasBlock(string $name, array $context = []): bool
{
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context + $this->context + $this->env->getGlobals());
return isset($blocks[$name]);
}
/**
* @param array<string, mixed> $context
*
* @return string[]
*/
public function getBlockNames(array $context = []): array
{
return array_keys($this->fixed ? $this->blocks : $this->resolveBlocks($context + $this->context + $this->env->getGlobals()));
}
/**
* @param array<string, mixed> $context
*
* @return iterable<scalar|\Stringable|null>
*/
public function streamBlock(string $name, array $context = []): iterable
{
$context += $this->context + $this->env->getGlobals();
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context);
if (!isset($blocks[$name])) {
$this->throwUnknownBlock($name);
}
yield from $this->entryTemplate()->yieldBlock($name, $context, $blocks);
}
/**
* @param array<string, mixed> $context
*/
public function renderBlock(string $name, array $context = []): string
{
$context += $this->context + $this->env->getGlobals();
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context);
if (!isset($blocks[$name])) {
$this->throwUnknownBlock($name);
}
return $this->entryTemplate()->renderBlock($name, $context, $blocks);
}
/**
* @param array<string, mixed> $context
*/
public function displayBlock(string $name, array $context = []): void
{
$context += $this->context + $this->env->getGlobals();
$blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context);
if (!isset($blocks[$name])) {
$this->throwUnknownBlock($name);
}
$this->entryTemplate()->displayBlock($name, $context, $blocks);
}
/**
* @param array<string, mixed> $context
*
* @return array<string, array{Template, string}>
*/
private function resolveBlocks(array $context): array
{
[$lineage, $fixed] = $this->resolveLineage($context);
$stale = $lineage !== $this->lineage;
foreach ($lineage as $template) {
if (!$template instanceof self) {
continue;
}
if (!$template->fixed) {
// a nested chain that can still move may resolve to other blocks on every call
$template->resolveBlocks($context + $template->context);
$stale = true;
}
$fixed = $fixed && $template->fixed;
}
$this->fixed = $fixed;
if (!$stale) {
return $this->blocks;
}
$blocks = [];
foreach ($lineage as $template) {
if ($template instanceof self) {
$blocks += $template->blocks;
continue;
}
foreach ($template->getBlocks() as $name => $block) {
$blocks[$name] ??= $block;
}
}
$this->lineage = $lineage;
return $this->blocks = $blocks;
}
/**
* @param array<string, mixed> $context
*
* @return array{list<Template|self>, bool}
*/
private function resolveLineage(array $context): array
{
$lineage = [];
$fixed = true;
foreach ($this->templates as $template) {
if ($template instanceof self) {
$lineage[] = $template;
continue;
}
$seen = [];
do {
if (isset($seen[$id = spl_object_id($template)])) {
throw new \LogicException(\sprintf('Circular template inheritance detected while building a block chain from "%s".', $template->getTemplateName()));
}
$seen[$id] = true;
$lineage[] = $template;
$parent = $template->getParent($context);
$fixed = $fixed && $template->hasFixedParent();
$template = $parent;
} while (false !== $template);
}
return [$lineage, $fixed];
}
private function entryTemplate(): Template
{
return ($template = $this->templates[0]) instanceof self ? $template->entryTemplate() : $template;
}
private function throwUnknownBlock(string $name): never
{
throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->entryTemplate()->getTemplateName()), -1, $this->entryTemplate()->getSourceContext());
}
}
+10 -1
View File
@@ -19,7 +19,7 @@ namespace Twig\Cache;
*
* @author Quentin Devos <quentin@devos.pm>
*/
final class ChainCache implements CacheInterface
final class ChainCache implements CacheInterface, RemovableCacheInterface
{
/**
* @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
@@ -69,6 +69,15 @@ final class ChainCache implements CacheInterface
return 0;
}
public function remove(string $name, string $cls): void
{
foreach ($this->caches as $cache) {
if ($cache instanceof RemovableCacheInterface) {
$cache->remove($name, $cls);
}
}
}
/**
* @return string[]
*/
@@ -16,7 +16,7 @@ namespace Twig\Cache;
*
* @author Andrew Tch <andrew@noop.lv>
*/
class FilesystemCache implements CacheInterface
class FilesystemCache implements CacheInterface, RemovableCacheInterface
{
public const FORCE_BYTECODE_INVALIDATION = 1;
@@ -76,6 +76,14 @@ class FilesystemCache implements CacheInterface
throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key));
}
public function remove(string $name, string $cls): void
{
$key = $this->generateKey($name, $cls);
if (!@unlink($key) && file_exists($key)) {
throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key));
}
}
public function getTimestamp(string $key): int
{
if (!is_file($key)) {
+5 -1
View File
@@ -16,7 +16,7 @@ namespace Twig\Cache;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class NullCache implements CacheInterface
final class NullCache implements CacheInterface, RemovableCacheInterface
{
public function generateKey(string $name, string $className): string
{
@@ -35,4 +35,8 @@ final class NullCache implements CacheInterface
{
return 0;
}
public function remove(string $name, string $cls): void
{
}
}
@@ -0,0 +1,20 @@
<?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\Cache;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface RemovableCacheInterface
{
public function remove(string $name, string $cls): void;
}
+12 -6
View File
@@ -74,7 +74,7 @@ class Compiler
$node->compile($this);
if ($this->didUseEcho) {
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class);
}
return $this;
@@ -99,7 +99,7 @@ class Compiler
$node->compile($this);
if ($this->didUseEcho) {
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class);
}
return $this;
@@ -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;
}
@@ -170,7 +176,7 @@ class Compiler
} elseif (\is_bool($value)) {
$this->raw($value ? 'true' : 'false');
} elseif (\is_array($value)) {
$this->raw('array(');
$this->raw('[');
$first = true;
foreach ($value as $key => $v) {
if (!$first) {
@@ -181,7 +187,7 @@ class Compiler
$this->raw(' => ');
$this->repr($v);
}
$this->raw(')');
$this->raw(']');
} else {
$this->string($value);
}
@@ -243,7 +249,7 @@ class Compiler
public function getVarName(): string
{
return \sprintf('__internal_compile_%d', $this->varNameSalt++);
return \sprintf('_v%d', $this->varNameSalt++);
}
private function checkForEcho(string $string): void
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DeprecatedCallableInfo
{
private string $type;
private string $name;
public function __construct(
private string $package,
private string $version,
private ?string $altName = null,
private ?string $altPackage = null,
private ?string $altVersion = null,
) {
}
public function setType(string $type): void
{
$this->type = $type;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function triggerDeprecation(?string $file = null, ?int $line = null): void
{
$message = \sprintf('Twig %s "%s" is deprecated', ucfirst($this->type), $this->name);
if ($this->altName) {
$message .= \sprintf('; use "%s"', $this->altName);
if ($this->altPackage) {
$message .= \sprintf(' from the "%s" package', $this->altPackage);
}
if ($this->altVersion) {
$message .= \sprintf(' (available since version %s)', $this->altVersion);
}
$message .= ' instead';
}
if ($file) {
$message .= \sprintf(' in %s', $file);
if ($line) {
$message .= \sprintf(' at line %d', $line);
}
}
$message .= '.';
trigger_deprecation($this->package, $this->version, $message);
}
}
+120 -34
View File
@@ -14,10 +14,12 @@ namespace Twig;
use Twig\Cache\CacheInterface;
use Twig\Cache\FilesystemCache;
use Twig\Cache\NullCache;
use Twig\Cache\RemovableCacheInterface;
use Twig\Error\Error;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\ExpressionParsers;
use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
use Twig\Extension\ExtensionInterface;
@@ -26,8 +28,6 @@ use Twig\Extension\YieldNotReadyExtension;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Twig\Loader\LoaderInterface;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\NodeVisitor\NodeVisitorInterface;
@@ -43,10 +43,10 @@ use Twig\TokenParser\TokenParserInterface;
*/
class Environment
{
public const VERSION = '3.14.0';
public const VERSION_ID = 31400;
public const VERSION = '3.30.0';
public const VERSION_ID = 33000;
public const MAJOR_VERSION = 3;
public const MINOR_VERSION = 14;
public const MINOR_VERSION = 30;
public const RELEASE_VERSION = 0;
public const EXTRA_VERSION = '';
@@ -71,6 +71,11 @@ class Environment
/** @var bool */
private $useYield;
private $defaultRuntimeLoader;
private array $hotCache = [];
/**
* @var array<string, TemplateWrapper>
*/
private array $loadedWrappers = [];
/**
* Constructor.
@@ -143,6 +148,11 @@ class Environment
$this->addExtension(new OptimizerExtension($options['optimizations']));
}
public function __clone()
{
trigger_deprecation('twig/twig', '3.30', 'Cloning a "%s" instance is deprecated and will throw in Twig 4.0; build a new environment instead.', self::class);
}
/**
* @internal
*/
@@ -153,20 +163,24 @@ class Environment
/**
* Enables debugging mode.
*
* @return void
*/
public function enableDebug()
{
$this->debug = true;
$this->updateOptionsHash();
$this->optionsHash = null;
}
/**
* Disables debugging mode.
*
* @return void
*/
public function disableDebug()
{
$this->debug = false;
$this->updateOptionsHash();
$this->optionsHash = null;
}
/**
@@ -181,6 +195,8 @@ class Environment
/**
* Enables the auto_reload option.
*
* @return void
*/
public function enableAutoReload()
{
@@ -189,6 +205,8 @@ class Environment
/**
* Disables the auto_reload option.
*
* @return void
*/
public function disableAutoReload()
{
@@ -207,20 +225,24 @@ class Environment
/**
* Enables the strict_variables option.
*
* @return void
*/
public function enableStrictVariables()
{
$this->strictVariables = true;
$this->updateOptionsHash();
$this->optionsHash = null;
}
/**
* Disables the strict_variables option.
*
* @return void
*/
public function disableStrictVariables()
{
$this->strictVariables = false;
$this->updateOptionsHash();
$this->optionsHash = null;
}
/**
@@ -233,6 +255,19 @@ class Environment
return $this->strictVariables;
}
public function removeCache(string $name): void
{
$cls = $this->getTemplateClass($name);
$this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16));
unset($this->loadedWrappers[$cls]);
if ($this->cache instanceof RemovableCacheInterface) {
$this->cache->remove($name, $cls);
} else {
throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache)));
}
}
/**
* Gets the current cache implementation.
*
@@ -253,6 +288,8 @@ class Environment
* @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation,
* an absolute path to the compiled templates,
* or false to disable cache
*
* @return void
*/
public function setCache($cache)
{
@@ -287,7 +324,7 @@ class Environment
*/
public function getTemplateClass(string $name, ?int $index = null): string
{
$key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
$key = ($this->hotCache[$name] ?? $this->getLoader()->getCacheKey($name)).($this->optionsHash ??= $this->getOptionsHash());
return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
}
@@ -332,6 +369,8 @@ class Environment
public function load($name): TemplateWrapper
{
if ($name instanceof TemplateWrapper) {
$name->unwrap($this);
return $name;
}
if ($name instanceof Template) {
@@ -340,7 +379,9 @@ class Environment
return $name;
}
return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
$cls = $this->getTemplateClass($name);
return $this->loadedWrappers[$cls] ??= new TemplateWrapper($this, $this->loadTemplate($cls, $name));
}
/**
@@ -379,8 +420,10 @@ class Environment
if (!class_exists($cls, false)) {
$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
$this->cache->write($key, $content);
$this->cache->load($key);
if (!isset($this->hotCache[$name])) {
$this->cache->write($key, $content);
$this->cache->load($key);
}
if (!class_exists($mainCls, false)) {
/* Last line of defense if either $this->bcWriteCacheFile was used,
@@ -474,7 +517,7 @@ class Environment
return new TemplateWrapper($this, $name);
}
if ($name instanceof TemplateWrapper) {
return $name;
return $this->load($name);
}
if (1 !== $count && !$this->getLoader()->exists($name)) {
@@ -487,6 +530,9 @@ class Environment
throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
}
/**
* @return void
*/
public function setLexer(Lexer $lexer)
{
$this->lexer = $lexer;
@@ -504,6 +550,9 @@ class Environment
return $this->lexer->tokenize($source);
}
/**
* @return void
*/
public function setParser(Parser $parser)
{
$this->parser = $parser;
@@ -523,6 +572,9 @@ class Environment
return $this->parser->parse($stream);
}
/**
* @return void
*/
public function setCompiler(Compiler $compiler)
{
$this->compiler = $compiler;
@@ -557,6 +609,9 @@ class Environment
}
}
/**
* @return void
*/
public function setLoader(LoaderInterface $loader)
{
$this->loader = $loader;
@@ -567,6 +622,9 @@ class Environment
return $this->loader;
}
/**
* @return void
*/
public function setCharset(string $charset)
{
if ('UTF8' === $charset = strtoupper($charset ?: '')) {
@@ -587,6 +645,9 @@ class Environment
return $this->extensionSet->hasExtension($class);
}
/**
* @return void
*/
public function addRuntimeLoader(RuntimeLoaderInterface $loader)
{
$this->runtimeLoaders[] = $loader;
@@ -634,19 +695,24 @@ class Environment
throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class));
}
/**
* @return void
*/
public function addExtension(ExtensionInterface $extension)
{
$this->extensionSet->addExtension($extension);
$this->updateOptionsHash();
$this->optionsHash = null;
}
/**
* @param ExtensionInterface[] $extensions An array of extensions
*
* @return void
*/
public function setExtensions(array $extensions)
{
$this->extensionSet->setExtensions($extensions);
$this->updateOptionsHash();
$this->optionsHash = null;
}
/**
@@ -657,6 +723,9 @@ class Environment
return $this->extensionSet->getExtensions();
}
/**
* @return void
*/
public function addTokenParser(TokenParserInterface $parser)
{
$this->extensionSet->addTokenParser($parser);
@@ -680,11 +749,17 @@ class Environment
return $this->extensionSet->getTokenParser($name);
}
/**
* @param callable(string): (TokenParserInterface|false) $callable
*/
public function registerUndefinedTokenParserCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedTokenParserCallback($callable);
}
/**
* @return void
*/
public function addNodeVisitor(NodeVisitorInterface $visitor)
{
$this->extensionSet->addNodeVisitor($visitor);
@@ -700,6 +775,9 @@ class Environment
return $this->extensionSet->getNodeVisitors();
}
/**
* @return void
*/
public function addFilter(TwigFilter $filter)
{
$this->extensionSet->addFilter($filter);
@@ -713,6 +791,9 @@ class Environment
return $this->extensionSet->getFilter($name);
}
/**
* @param callable(string): (TwigFilter|false) $callable
*/
public function registerUndefinedFilterCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedFilterCallback($callable);
@@ -734,6 +815,9 @@ class Environment
return $this->extensionSet->getFilters();
}
/**
* @return void
*/
public function addTest(TwigTest $test)
{
$this->extensionSet->addTest($test);
@@ -757,6 +841,17 @@ class Environment
return $this->extensionSet->getTest($name);
}
/**
* @param callable(string): (TwigTest|false) $callable
*/
public function registerUndefinedTestCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedTestCallback($callable);
}
/**
* @return void
*/
public function addFunction(TwigFunction $function)
{
$this->extensionSet->addFunction($function);
@@ -770,6 +865,9 @@ class Environment
return $this->extensionSet->getFunction($name);
}
/**
* @param callable(string): (TwigFunction|false) $callable
*/
public function registerUndefinedFunctionCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedFunctionCallback($callable);
@@ -798,6 +896,8 @@ class Environment
* but after, you can only update existing globals.
*
* @param mixed $value The global value
*
* @return void
*/
public function addGlobal(string $name, $value)
{
@@ -813,8 +913,6 @@ class Environment
}
/**
* @internal
*
* @return array<string, mixed>
*/
public function getGlobals(): array
@@ -848,27 +946,15 @@ class Environment
/**
* @internal
*
* @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
*/
public function getUnaryOperators(): array
public function getExpressionParsers(): ExpressionParsers
{
return $this->extensionSet->getUnaryOperators();
return $this->extensionSet->getExpressionParsers();
}
/**
* @internal
*
* @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
private function getOptionsHash(): string
{
return $this->extensionSet->getBinaryOperators();
}
private function updateOptionsHash(): void
{
$this->optionsHash = implode(':', [
return implode(':', [
$this->extensionSet->getSignature(),
\PHP_MAJOR_VERSION,
\PHP_MINOR_VERSION,
+69 -88
View File
@@ -29,44 +29,39 @@ use Twig\Template;
* Whenever possible, you must set these information (original template name
* and line number) yourself by passing them to the constructor. If some or all
* these information are not available from where you throw the exception, then
* this class will guess them automatically (when the line number is set to -1
* and/or the name is set to null). As this is a costly operation, this
* can be disabled by passing false for both the name and the line number
* when creating a new instance of this class.
* this class will guess them automatically.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Error extends \Exception
{
private $lineno;
private $name;
/** @var positive-int|null */
private ?int $columnno;
private $rawMessage;
private $sourcePath;
private $sourceCode;
private ?Source $source;
private string $phpFile;
private int $phpLine;
/**
* Constructor.
*
* 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);
if (null === $source) {
$name = null;
} else {
$name = $source->getName();
$this->sourceCode = $source->getCode();
$this->sourcePath = $source->getPath();
}
$this->phpFile = $this->getFile();
$this->phpLine = $this->getLine();
$this->lineno = $lineno;
$this->name = $name;
$this->columnno = $columnno;
$this->source = $source;
$this->rawMessage = $message;
$this->updateRepr();
}
@@ -84,30 +79,45 @@ class Error extends \Exception
public function setTemplateLine(int $lineno): void
{
$this->lineno = $lineno;
$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->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null;
return $this->source;
}
public function setSourceContext(?Source $source = null): void
{
if (null === $source) {
$this->sourceCode = $this->name = $this->sourcePath = null;
} else {
$this->sourceCode = $source->getCode();
$this->name = $source->getName();
$this->sourcePath = $source->getPath();
}
$this->source = $source;
$this->updateRepr();
}
public function guess(): void
{
if ($this->lineno > -1) {
return;
}
$this->guessTemplateInfo();
$this->updateRepr();
}
@@ -120,80 +130,52 @@ class Error extends \Exception
private function updateRepr(): void
{
$this->message = $this->rawMessage;
if ($this->sourcePath && $this->lineno > 0) {
$this->file = $this->sourcePath;
$this->line = $this->lineno;
return;
}
$dot = false;
if (str_ends_with($this->message, '.')) {
$this->message = substr($this->message, 0, -1);
$dot = true;
}
$questionMark = false;
if (str_ends_with($this->message, '?')) {
$this->message = substr($this->message, 0, -1);
$questionMark = true;
}
if ($this->name) {
if (\is_string($this->name) || $this->name instanceof \Stringable) {
$name = \sprintf('"%s"', $this->name);
if ($this->source && $this->source->getPath()) {
// we only update the file and the line together
$this->file = $this->source->getPath();
if ($this->lineno > 0) {
$this->line = $this->lineno;
} else {
$name = json_encode($this->name);
$this->line = -1;
}
$this->message .= \sprintf(' in %s', $name);
}
if ($this->lineno && $this->lineno >= 0) {
$this->message = $this->rawMessage;
$last = substr($this->message, -1);
if ($punctuation = '.' === $last || '?' === $last ? $last : '') {
$this->message = substr($this->message, 0, -1);
}
if ($this->source && $this->source->getName()) {
$this->message .= \sprintf(' in "%s"', $this->source->getName());
}
if ($this->lineno > 0) {
$this->message .= \sprintf(' at line %d', $this->lineno);
if (null !== $this->columnno) {
$this->message .= \sprintf(' column %d', $this->columnno);
}
}
if ($dot) {
$this->message .= '.';
}
if ($questionMark) {
$this->message .= '?';
if ($punctuation) {
$this->message .= $punctuation;
}
}
private function guessTemplateInfo(): void
{
$template = null;
$templateClass = null;
// $this->source is never null here (see guess() usage in Template)
$this->lineno = 0;
$template = null;
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT);
foreach ($backtrace as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof Template) {
$currentClass = \get_class($trace['object']);
$isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass);
if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
$template = $trace['object'];
$templateClass = \get_class($trace['object']);
}
if (isset($trace['object']) && $trace['object'] instanceof Template && $this->source->getName() === $trace['object']->getTemplateName()) {
$template = $trace['object'];
break;
}
}
// update template name
if (null !== $template && null === $this->name) {
$this->name = $template->getTemplateName();
}
// update template path if any
if (null !== $template && null === $this->sourcePath) {
$src = $template->getSourceContext();
$this->sourceCode = $src->getCode();
$this->sourcePath = $src->getPath();
}
if (null === $template || $this->lineno > -1) {
return;
if (null === $template) {
return; // Impossible to guess the info as the template was not found in the backtrace
}
$r = new \ReflectionObject($template);
@@ -206,8 +188,7 @@ class Error extends \Exception
while ($e = array_pop($exceptions)) {
$traces = $e->getTrace();
array_unshift($traces, ['file' => $e->getFile(), 'line' => $e->getLine()]);
array_unshift($traces, ['file' => $e instanceof self ? $e->phpFile : $e->getFile(), 'line' => $e instanceof self ? $e->phpLine : $e->getLine()]);
while ($trace = array_shift($traces)) {
if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
continue;
+142 -626
View File
@@ -12,25 +12,19 @@
namespace Twig;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\ExpressionParser\Infix\DotExpressionParser;
use Twig\ExpressionParser\Infix\FilterExpressionParser;
use Twig\ExpressionParser\Infix\SquareBracketExpressionParser;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ArrowFunctionExpression;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\TestExpression;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
use Twig\Node\Nodes;
/**
* Parses expressions.
@@ -41,400 +35,108 @@ use Twig\Node\Node;
* @see https://en.wikipedia.org/wiki/Operator-precedence_parser
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Twig 3.21
*/
class ExpressionParser
{
/**
* @deprecated since Twig 3.21
*/
public const OPERATOR_LEFT = 1;
/**
* @deprecated since Twig 3.21
*/
public const OPERATOR_RIGHT = 2;
/** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
private $unaryOperators;
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
private $binaryOperators;
private $readyNodes = [];
public function __construct(
private Parser $parser,
private Environment $env,
) {
$this->unaryOperators = $env->getUnaryOperators();
$this->binaryOperators = $env->getBinaryOperators();
trigger_deprecation('twig/twig', '3.21', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__);
}
public function parseExpression($precedence = 0, $allowArrow = false)
public function parseExpression($precedence = 0)
{
if ($allowArrow && $arrow = $this->parseArrow()) {
return $arrow;
if (\func_num_args() > 1) {
trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__);
}
$expr = $this->getPrimary();
$token = $this->parser->getCurrentToken();
while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
$op = $this->binaryOperators[$token->getValue()];
$this->parser->getStream()->next();
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__);
if ('is not' === $token->getValue()) {
$expr = $this->parseNotTestExpression($expr);
} elseif ('is' === $token->getValue()) {
$expr = $this->parseTestExpression($expr);
} elseif (isset($op['callable'])) {
$expr = $op['callable']($this->parser, $expr);
} else {
$expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true);
$class = $op['class'];
$expr = new $class($expr, $expr1, $token->getLine());
}
$token = $this->parser->getCurrentToken();
}
if (0 === $precedence) {
return $this->parseConditionalExpression($expr);
}
return $expr;
return $this->parser->parseExpression((int) $precedence);
}
/**
* @return ArrowFunctionExpression|null
* @deprecated since Twig 3.21
*/
private function parseArrow()
{
$stream = $this->parser->getStream();
// short array syntax (one argument, no parentheses)?
if ($stream->look(1)->test(Token::ARROW_TYPE)) {
$line = $stream->getCurrent()->getLine();
$token = $stream->expect(Token::NAME_TYPE);
$names = [new AssignNameExpression($token->getValue(), $token->getLine())];
$stream->expect(Token::ARROW_TYPE);
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
}
// first, determine if we are parsing an arrow function by finding => (long form)
$i = 0;
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
return null;
}
++$i;
while (true) {
// variable name
++$i;
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
break;
}
++$i;
}
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
return null;
}
++$i;
if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
return null;
}
// yes, let's parse it properly
$token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
$line = $token->getLine();
$names = [];
while (true) {
$token = $stream->expect(Token::NAME_TYPE);
$names[] = new AssignNameExpression($token->getValue(), $token->getLine());
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
$stream->expect(Token::PUNCTUATION_TYPE, ')');
$stream->expect(Token::ARROW_TYPE);
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
}
private function getPrimary(): AbstractExpression
{
$token = $this->parser->getCurrentToken();
if ($this->isUnary($token)) {
$operator = $this->unaryOperators[$token->getValue()];
$this->parser->getStream()->next();
$expr = $this->parseExpression($operator['precedence']);
$class = $operator['class'];
return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
} elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) {
$this->parser->getStream()->next();
$expr = $this->parseExpression();
$this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
return $this->parsePostfixExpression($expr);
}
return $this->parsePrimaryExpression();
}
private function parseConditionalExpression($expr): AbstractExpression
{
while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) {
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$expr2 = $this->parseExpression();
if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
// Ternary operator (expr ? expr2 : expr3)
$expr3 = $this->parseExpression();
} else {
// Ternary without else (expr ? expr2)
$expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
}
} else {
// Ternary without then (expr ?: expr3)
$expr2 = $expr;
$expr3 = $this->parseExpression();
}
$expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
}
return $expr;
}
private function isUnary(Token $token): bool
{
return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
}
private function isBinary(Token $token): bool
{
return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
}
public function parsePrimaryExpression()
{
$token = $this->parser->getCurrentToken();
switch ($token->getType()) {
case Token::NAME_TYPE:
$this->parser->getStream()->next();
switch ($token->getValue()) {
case 'true':
case 'TRUE':
$node = new ConstantExpression(true, $token->getLine());
break;
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
case 'false':
case 'FALSE':
$node = new ConstantExpression(false, $token->getLine());
break;
case 'none':
case 'NONE':
case 'null':
case 'NULL':
$node = new ConstantExpression(null, $token->getLine());
break;
default:
if ('(' === $this->parser->getCurrentToken()->getValue()) {
$node = $this->getFunctionNode($token->getValue(), $token->getLine());
} else {
$node = new NameExpression($token->getValue(), $token->getLine());
}
}
break;
case Token::NUMBER_TYPE:
$this->parser->getStream()->next();
$node = new ConstantExpression($token->getValue(), $token->getLine());
break;
case Token::STRING_TYPE:
case Token::INTERPOLATION_START_TYPE:
$node = $this->parseStringExpression();
break;
case Token::OPERATOR_TYPE:
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
// in this context, string operators are variable names
$this->parser->getStream()->next();
$node = new NameExpression($token->getValue(), $token->getLine());
break;
}
if (isset($this->unaryOperators[$token->getValue()])) {
$class = $this->unaryOperators[$token->getValue()]['class'];
if (!\in_array($class, [NegUnary::class, PosUnary::class])) {
throw new SyntaxError(\sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
}
$this->parser->getStream()->next();
$expr = $this->parsePrimaryExpression();
$node = new $class($expr, $token->getLine());
break;
}
// no break
default:
if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
$node = $this->parseSequenceExpression();
} elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
$node = $this->parseMappingExpression();
} elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
} else {
throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
}
}
return $this->parsePostfixExpression($node);
}
public function parseStringExpression()
{
$stream = $this->parser->getStream();
$nodes = [];
// a string cannot be followed by another string in a single expression
$nextCanBeString = true;
while (true) {
if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
$nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
$nextCanBeString = false;
} elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
$nodes[] = $this->parseExpression();
$stream->expect(Token::INTERPOLATION_END_TYPE);
$nextCanBeString = true;
} else {
break;
}
}
$expr = array_shift($nodes);
foreach ($nodes as $node) {
$expr = new ConcatBinary($expr, $node, $node->getTemplateLine());
}
return $expr;
return $this->parseExpression();
}
/**
* @deprecated since 3.11, use parseSequenceExpression() instead
* @deprecated since Twig 3.21
*/
public function parseStringExpression()
{
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
return $this->parseExpression();
}
/**
* @deprecated since Twig 3.11, use parseExpression() instead
*/
public function parseArrayExpression()
{
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__);
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__);
return $this->parseSequenceExpression();
}
public function parseSequenceExpression()
{
$stream = $this->parser->getStream();
$stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma');
// trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
break;
}
}
$first = false;
if ($stream->test(Token::SPREAD_TYPE)) {
$stream->next();
$expr = $this->parseExpression();
$expr->setAttribute('spread', true);
$node->addElement($expr);
} else {
$node->addElement($this->parseExpression());
}
}
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
return $node;
return $this->parseExpression();
}
/**
* @deprecated since 3.11, use parseMappingExpression() instead
* @deprecated since Twig 3.21
*/
public function parseSequenceExpression()
{
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
return $this->parseExpression();
}
/**
* @deprecated since Twig 3.11, use parseExpression() instead
*/
public function parseHashExpression()
{
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__);
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__);
return $this->parseMappingExpression();
return $this->parseExpression();
}
/**
* @deprecated since Twig 3.21
*/
public function parseMappingExpression()
{
$stream = $this->parser->getStream();
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma');
// trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
break;
}
}
$first = false;
if ($stream->test(Token::SPREAD_TYPE)) {
$stream->next();
$value = $this->parseExpression();
$value->setAttribute('spread', true);
$node->addElement($value);
continue;
}
// a mapping key can be:
//
// * a number -- 12
// * a string -- 'a'
// * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2)
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$key = new ConstantExpression($token->getValue(), $token->getLine());
// {a} is a shortcut for {a:a}
if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) {
$value = new NameExpression($key->getAttribute('value'), $key->getTemplateLine());
$node->addElement($value, $key);
continue;
}
} elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) {
$key = new ConstantExpression($token->getValue(), $token->getLine());
} elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$key = $this->parseExpression();
} else {
$current = $stream->getCurrent();
throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
}
$stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)');
$value = $this->parseExpression();
$node->addElement($value, $key);
}
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
return $node;
return $this->parseExpression();
}
/**
* @deprecated since Twig 3.21
*/
public function parsePostfixExpression($node)
{
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
while (true) {
$token = $this->parser->getCurrentToken();
if (Token::PUNCTUATION_TYPE == $token->getType()) {
if ($token->test(Token::PUNCTUATION_TYPE)) {
if ('.' == $token->getValue() || '[' == $token->getValue()) {
$node = $this->parseSubscriptExpression($node);
} elseif ('|' == $token->getValue()) {
@@ -450,154 +152,49 @@ class ExpressionParser
return $node;
}
public function getFunctionNode($name, $line)
{
if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
$arguments = new ArrayExpression([], $line);
foreach ($this->parseArguments() as $n) {
$arguments->addElement($n);
}
$node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
$node->setAttribute('safe', true);
return $node;
}
$args = $this->parseArguments(true);
$function = $this->getFunction($name, $line);
if ($function->getParserCallable()) {
$fakeNode = new Node(lineno: $line);
$fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line);
}
if (!isset($this->readyNodes[$class = $function->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
return new $class($ready ? $function : $function->getName(), $args, $line);
}
/**
* @deprecated since Twig 3.21
*/
public function parseSubscriptExpression($node)
{
$stream = $this->parser->getStream();
$token = $stream->next();
$lineno = $token->getLine();
$arguments = new ArrayExpression([], $lineno);
$type = Template::ANY_CALL;
if ('.' == $token->getValue()) {
$token = $stream->next();
if (
Token::NAME_TYPE == $token->getType()
||
Token::NUMBER_TYPE == $token->getType()
||
(Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
) {
$arg = new ConstantExpression($token->getValue(), $lineno);
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$type = Template::METHOD_CALL;
foreach ($this->parseArguments() as $n) {
$arguments->addElement($n);
}
}
} else {
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
}
$parsers = new \ReflectionProperty($this->parser, 'parsers');
if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
$name = $arg->getAttribute('value');
$node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
$node->setAttribute('safe', true);
return $node;
}
} else {
$type = Template::ARRAY_CALL;
// slice?
$slice = false;
if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
$arg = new ConstantExpression(0, $token->getLine());
} else {
$arg = $this->parseExpression();
}
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
}
if ($slice) {
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
$length = new ConstantExpression(null, $token->getLine());
} else {
$length = $this->parseExpression();
}
$filter = $this->getFilter('slice', $token->getLine());
$arguments = new Node([$arg, $length]);
$filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());
$stream->expect(Token::PUNCTUATION_TYPE, ']');
return $filter;
}
$stream->expect(Token::PUNCTUATION_TYPE, ']');
if ('.' === $this->parser->getStream()->next()->getValue()) {
return $parsers->getValue($this->parser)->getByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken());
}
return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
return $parsers->getValue($this->parser)->getByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken());
}
/**
* @deprecated since Twig 3.21
*/
public function parseFilterExpression($node)
{
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
$this->parser->getStream()->next();
return $this->parseFilterExpressionRaw($node);
}
/**
* @deprecated since Twig 3.21
*/
public function parseFilterExpressionRaw($node)
{
if (\func_num_args() > 1) {
trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
}
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
$parsers = new \ReflectionProperty($this->parser, 'parsers');
$op = $parsers->getValue($this->parser)->getByClass(FilterExpressionParser::class);
while (true) {
$token = $this->parser->getStream()->expect(Token::NAME_TYPE);
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
$arguments = new Node();
} else {
$arguments = $this->parseArguments(true, false, true);
}
$filter = $this->getFilter($token->getValue(), $token->getLine());
$ready = true;
if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
$node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine());
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) {
$node = $op->parse($this->parser, $node, $this->parser->getCurrentToken());
if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) {
break;
}
$this->parser->getStream()->next();
}
@@ -607,21 +204,35 @@ class ExpressionParser
/**
* Parses arguments.
*
* @param bool $namedArguments Whether to allow named arguments or not
* @param bool $definition Whether we are parsing arguments for a function (or macro) definition
*
* @return Node
*
* @throws SyntaxError
*
* @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead
*/
public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
public function parseArguments()
{
trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__));
$parsePrimaryExpression = new \ReflectionMethod($this->parser, 'parsePrimaryExpression');
$namedArguments = false;
$definition = false;
if (\func_num_args() > 1) {
$definition = func_get_arg(1);
}
if (\func_num_args() > 0) {
trigger_deprecation('twig/twig', '3.15', 'Passing arguments to "%s()" is deprecated.', __METHOD__);
$namedArguments = func_get_arg(0);
}
$args = [];
$stream = $this->parser->getStream();
$stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
$stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
$hasSpread = false;
while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
if (!empty($args)) {
if ($args) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
// if the comma above was a trailing comma, early exit the argument parse loop
@@ -632,26 +243,33 @@ class ExpressionParser
if ($definition) {
$token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
$value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
$value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine());
} else {
$value = $this->parseExpression(0, $allowArrow);
if ($stream->nextIf(Token::SPREAD_TYPE)) {
$hasSpread = true;
$value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine());
} elseif ($hasSpread) {
throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
} else {
$value = $this->parseExpression();
}
}
$name = null;
if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) {
if (!$value instanceof NameExpression) {
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) {
if (!$value instanceof ContextVariable) {
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext());
}
$name = $value->getAttribute('name');
if ($definition) {
$value = $this->parsePrimaryExpression();
$value = $parsePrimaryExpression->invoke($this->parser);
if (!$this->checkConstantExpression($value)) {
throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext());
}
} else {
$value = $this->parseExpression(0, $allowArrow);
$value = $this->parseExpression();
}
}
@@ -672,11 +290,16 @@ class ExpressionParser
}
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
return new Node($args);
return new Nodes($args);
}
/**
* @deprecated since Twig 3.21, use "AbstractTokenParser::parseAssignmentExpression()" instead
*/
public function parseAssignmentExpression()
{
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__);
$stream = $this->parser->getStream();
$targets = [];
while (true) {
@@ -687,22 +310,23 @@ class ExpressionParser
} else {
$stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
}
$value = $token->getValue();
if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) {
throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
}
$targets[] = new AssignNameExpression($value, $token->getLine());
$targets[] = new AssignContextVariable($token->getValue(), $token->getLine());
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
return new Node($targets);
return new Nodes($targets);
}
/**
* @deprecated since Twig 3.21
*/
public function parseMultitargetExpression()
{
trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__);
$targets = [];
while (true) {
$targets[] = $this->parseExpression();
@@ -711,129 +335,11 @@ class ExpressionParser
}
}
return new Node($targets);
}
private function parseNotTestExpression(Node $node): NotUnary
{
return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
}
private function parseTestExpression(Node $node): TestExpression
{
$stream = $this->parser->getStream();
$test = $this->getTest($node->getTemplateLine());
$arguments = null;
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$arguments = $this->parseArguments(true);
} elseif ($test->hasOneMandatoryArgument()) {
$arguments = new Node([0 => $this->parsePrimaryExpression()]);
}
if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
$node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
$node->setAttribute('safe', true);
}
$ready = $test instanceof TwigTest;
if (!isset($this->readyNodes[$class = $test->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine());
}
private function getTest(int $line): TwigTest
{
$stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue();
if ($stream->test(Token::NAME_TYPE)) {
// try 2-words tests
$name = $name.' '.$this->parser->getCurrentToken()->getValue();
if ($test = $this->env->getTest($name)) {
$stream->next();
}
} else {
$test = $this->env->getTest($name);
}
if (!$test) {
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getTests()));
throw $e;
}
if ($test->isDeprecated()) {
$stream = $this->parser->getStream();
$message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
if ($test->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $test->getAlternative());
}
$src = $stream->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
}
return $test;
}
private function getFunction(string $name, int $line): TwigFunction
{
if (!$function = $this->env->getFunction($name)) {
$e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getFunctions()));
throw $e;
}
if ($function->isDeprecated()) {
$message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
if ($function->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $function->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
}
return $function;
}
private function getFilter(string $name, int $line): TwigFilter
{
if (!$filter = $this->env->getFilter($name)) {
$e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getFilters()));
throw $e;
}
if ($filter->isDeprecated()) {
$message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
if ($filter->getAlternative()) {
$message .= \sprintf('. Use "%s" instead', $filter->getAlternative());
}
$src = $this->parser->getStream()->getSourceContext();
$message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
}
return $filter;
return new Nodes($targets);
}
// checks that the node only contains "constant" elements
// to be removed in 4.0
private function checkConstantExpression(Node $node): bool
{
if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
@@ -850,4 +356,14 @@ class ExpressionParser
return true;
}
/**
* @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead
*/
public function parseOnlyArguments()
{
trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__));
return $this->parseArguments();
}
}
@@ -0,0 +1,35 @@
<?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\ExpressionParser;
abstract class AbstractExpressionParser implements ExpressionParserInterface
{
public function __toString(): string
{
return \sprintf('%s(%s)', ExpressionParserType::getType($this)->value, $this->getName());
}
public function getPrecedenceChange(): ?PrecedenceChange
{
return null;
}
public function getAliases(): array
{
return [];
}
public function getOperatorTokens(): array
{
return [$this->getName(), ...$this->getAliases()];
}
}
@@ -0,0 +1,17 @@
<?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\ExpressionParser;
interface ExpressionParserDescriptionInterface
{
public function getDescription(): string;
}
@@ -0,0 +1,36 @@
<?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\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;
public function getName(): string;
public function getPrecedence(): int;
public function getPrecedenceChange(): ?PrecedenceChange;
/**
* @return array<string>
*/
public function getAliases(): array;
}
@@ -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\ExpressionParser;
/**
* @internal
*/
enum ExpressionParserType: string
{
case Prefix = 'prefix';
case Infix = 'infix';
public static function getType(object $object): ExpressionParserType
{
if ($object instanceof PrefixExpressionParserInterface) {
return self::Prefix;
}
if ($object instanceof InfixExpressionParserInterface) {
return self::Infix;
}
throw new \InvalidArgumentException(\sprintf('Unsupported expression parser type: %s', $object::class));
}
}
@@ -0,0 +1,155 @@
<?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\ExpressionParser;
/**
* @template-implements \IteratorAggregate<ExpressionParserInterface>
*
* @internal
*/
final class ExpressionParsers implements \IteratorAggregate
{
/**
* @var array<class-string<ExpressionParserInterface>, array<string, ExpressionParserInterface>>
*/
private array $parsersByName = [];
/**
* @var array<class-string<ExpressionParserInterface>, ExpressionParserInterface>
*/
private array $parsersByClass = [];
/**
* @var \WeakMap<ExpressionParserInterface, array<ExpressionParserInterface>>|null
*/
private ?\WeakMap $precedenceChanges = null;
/**
* @param array<ExpressionParserInterface> $parsers
*/
public function __construct(array $parsers = [])
{
$this->add($parsers);
}
/**
* @param array<ExpressionParserInterface> $parsers
*
* @return $this
*/
public function add(array $parsers): static
{
foreach ($parsers as $parser) {
if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) {
trigger_deprecation('twig/twig', '3.21', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence());
// 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->parsersByClass[$parser::class] = $parser;
foreach (self::getOperatorTokensFor($parser) as $token) {
$this->parsersByName[$interface][$token] = $parser;
}
}
return $this;
}
/**
* @template T of ExpressionParserInterface
*
* @param class-string<T> $class
*
* @return T|null
*/
public function getByClass(string $class): ?ExpressionParserInterface
{
return $this->parsersByClass[$class] ?? null;
}
/**
* @template T of ExpressionParserInterface
*
* @param class-string<T> $interface
*
* @return T|null
*/
public function getByName(string $interface, string $name): ?ExpressionParserInterface
{
return $this->parsersByName[$interface][$name] ?? null;
}
public function getIterator(): \Traversable
{
$seen = [];
foreach ($this->parsersByName as $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;
}
}
}
/**
* @internal
*
* @return \WeakMap<ExpressionParserInterface, array<ExpressionParserInterface>>
*/
public function getPrecedenceChanges(): \WeakMap
{
if (null === $this->precedenceChanges) {
$this->precedenceChanges = new \WeakMap();
foreach ($this as $ep) {
if (!$ep->getPrecedenceChange()) {
continue;
}
$min = min($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence());
$max = max($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence());
foreach ($this as $e) {
if ($e->getPrecedence() > $min && $e->getPrecedence() < $max) {
if (!isset($this->precedenceChanges[$e])) {
$this->precedenceChanges[$e] = [];
}
$this->precedenceChanges[$e][] = $ep;
}
}
}
}
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()];
}
}
@@ -0,0 +1,84 @@
<?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\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Expression\Variable\LocalVariable;
use Twig\Node\Nodes;
use Twig\Parser;
use Twig\Token;
trait ArgumentsTrait
{
private function parseCallableArguments(Parser $parser, int $line, bool $parseOpenParenthesis = true, bool $preserveNames = false): ArrayExpression
{
$arguments = new ArrayExpression([], $line);
foreach ($this->parseNamedArguments($parser, $parseOpenParenthesis) as $k => $n) {
$arguments->addElement($n, \is_int($k) || !$preserveNames ? new LocalVariable($k, $line) : new ConstantExpression($k, $line));
}
return $arguments;
}
private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis = true): Nodes
{
$args = [];
$stream = $parser->getStream();
if ($parseOpenParenthesis) {
$stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
}
$hasSpread = false;
while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
if ($args) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
// if the comma above was a trailing comma, early exit the argument parse loop
if ($stream->test(Token::PUNCTUATION_TYPE, ')')) {
break;
}
}
$value = $parser->parseExpression();
if ($value instanceof SpreadUnary) {
$hasSpread = true;
} elseif ($hasSpread) {
throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
$name = null;
if ($value instanceof SetBinary) {
$name = $value->getNode('left')->getAttribute('name');
$value = $value->getNode('right');
} elseif (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
if (!$value instanceof ContextVariable) {
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext());
}
$name = $value->getAttribute('name');
$value = $parser->parseExpression();
}
if (null === $name) {
$args[] = $value;
} else {
$args[$name] = $value;
}
}
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
return new Nodes($args);
}
}
@@ -0,0 +1,53 @@
<?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\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrowFunctionExpression;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
// As the expression of the arrow function is independent from the current precedence, we want a precedence of 0
return new ArrowFunctionExpression($parser->parseExpression(), $expr, $token->getLine());
}
public function getName(): string
{
return '=>';
}
public function getDescription(): string
{
return 'Arrow function (x => expr)';
}
public function getPrecedence(): int
{
return 250;
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
}
}
@@ -0,0 +1,80 @@
<?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\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ObjectDestructuringSetBinary;
use Twig\Node\Expression\Binary\SequenceDestructuringSetBinary;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
class AssignmentExpressionParser extends BinaryOperatorExpressionParser
{
public function __construct(
string $name,
) {
parent::__construct(SetBinary::class, $name, 0, InfixAssociativity::Right);
}
/**
* @return AbstractBinary
*/
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
{
if (!$left instanceof ContextVariable && !$left instanceof ArrayExpression) {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned.', $left::class), $token->getLine(), $parser->getStream()->getSourceContext());
}
$right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence());
$right = match ($this->getName()) {
'=' => $right,
default => throw new \LogicException(\sprintf('Unknown operator: %s.', $this->getName())),
};
if ($left instanceof ArrayExpression) {
$pairs = $left->getKeyValuePairs();
$isSequence = $left->isSequence();
if (!$pairs || ($isSequence && !array_filter($pairs, static fn (array $pair): bool => !$pair['value'] instanceof EmptyExpression))) {
throw new SyntaxError('Cannot destructure to an empty list of variables.', $token->getLine(), $parser->getStream()->getSourceContext());
}
foreach ($pairs as $i => $pair) {
if ($pair['value'] instanceof ContextVariable && !$pair['value'] instanceof AssignContextVariable) {
$left->setNode(2 * $i + 1, new AssignContextVariable($pair['value']->getAttribute('name'), $pair['value']->getTemplateLine()));
}
}
if ($isSequence) {
return new SequenceDestructuringSetBinary($left, $right, $token->getLine());
}
return new ObjectDestructuringSetBinary($left, $right, $token->getLine());
}
return new SetBinary($left, $right, $token->getLine());
}
public function getDescription(): string
{
return 'Assignment operator';
}
}
@@ -0,0 +1,80 @@
<?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\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function __construct(
/** @var class-string<AbstractBinary> */
private string $nodeClass,
private string $name,
private int $precedence,
private InfixAssociativity $associativity = InfixAssociativity::Left,
private ?PrecedenceChange $precedenceChange = null,
private ?string $description = null,
private array $aliases = [],
) {
}
/**
* @return AbstractBinary
*/
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
{
$right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence());
return new ($this->nodeClass)($left, $right, $token->getLine());
}
public function getAssociativity(): InfixAssociativity
{
return $this->associativity;
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description ?? '';
}
public function getPrecedence(): int
{
return $this->precedence;
}
public function getPrecedenceChange(): ?PrecedenceChange
{
return $this->precedenceChange;
}
public function getAliases(): array
{
return $this->aliases;
}
}
@@ -0,0 +1,62 @@
<?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\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Ternary\ConditionalTernary;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
{
$then = $parser->parseExpression($this->getPrecedence());
if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
// Ternary operator (expr ? expr2 : expr3)
$else = $parser->parseExpression($this->getPrecedence());
} else {
// Ternary without else (expr ? expr2)
$else = new ConstantExpression('', $token->getLine());
}
return new ConditionalTernary($left, $then, $else, $token->getLine());
}
public function getName(): string
{
return '?';
}
public function getDescription(): string
{
return 'Conditional operator (a ? b : c)';
}
public function getPrecedence(): int
{
return 0;
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
}
}
@@ -0,0 +1,109 @@
<?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\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Lexer;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Variable\MacroVariable;
use Twig\Parser;
use Twig\Template;
use Twig\Token;
/**
* @internal
*/
final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
$nullSafe = '?.' === $token->getValue();
$stream = $parser->getStream();
$token = $stream->getCurrent();
$lineno = $token->getLine();
$arguments = new ArrayExpression([], $lineno);
$type = Template::ANY_CALL;
if ($stream->nextIf(Token::OPERATOR_TYPE, '(')) {
$attribute = $parser->parseExpression();
$stream->expect(Token::PUNCTUATION_TYPE, ')');
} else {
$token = $stream->next();
if (
$token->test(Token::NAME_TYPE)
|| $token->test(Token::NUMBER_TYPE)
|| ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue()))
) {
$attribute = new ConstantExpression($token->getValue(), $token->getLine());
} else {
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type "%s".', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext());
}
}
$isMacroTarget = $expr instanceof NameExpression
&& (
null !== $parser->getImportedSymbol('template', $expr->getAttribute('name'))
|| '_self' === $expr->getAttribute('name')
);
if ($stream->test(Token::OPERATOR_TYPE, '(')) {
$type = Template::METHOD_CALL;
$arguments = $this->parseCallableArguments($parser, $token->getLine(), preserveNames: $isMacroTarget);
}
if ($isMacroTarget) {
$name = $attribute instanceof ConstantExpression ? (string) $attribute->getAttribute('value') : $attribute;
$node = new MacroReferenceExpression(new MacroVariable($expr->getAttribute('name'), $expr->getTemplateLine()), $name, $arguments, $expr->getTemplateLine());
$node->setHasCallParentheses(Template::METHOD_CALL === $type);
return $node;
}
return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $nullSafe);
}
public function getName(): string
{
return '.';
}
public function getAliases(): array
{
return ['?.'];
}
public function getDescription(): string
{
return 'Get an attribute on a variable';
}
public function getPrecedence(): int
{
return 512;
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
}
}
@@ -0,0 +1,85 @@
<?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\ExpressionParser\Infix;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
private $readyNodes = [];
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
$stream = $parser->getStream();
$token = $stream->expect(Token::NAME_TYPE);
$line = $token->getLine();
if (!$stream->test(Token::OPERATOR_TYPE, '(')) {
$arguments = new EmptyNode();
} else {
$arguments = $this->parseNamedArguments($parser);
}
$filter = $parser->getFilter($token->getValue(), $line);
$ready = true;
if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
return new $class($expr, $ready ? $filter : new ConstantExpression($filter->getName(), $line), $arguments, $line);
}
public function getName(): string
{
return '|';
}
public function getDescription(): string
{
return 'Twig filter call';
}
public function getPrecedence(): int
{
return 512;
}
public function getPrecedenceChange(): ?PrecedenceChange
{
return new PrecedenceChange('twig/twig', '3.21', 300);
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
}
}
@@ -0,0 +1,105 @@
<?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\ExpressionParser\Infix;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
private $readyNodes = [];
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
$line = $token->getLine();
if (!$expr instanceof NameExpression) {
throw new SyntaxError('Function name must be an identifier.', $line, $parser->getStream()->getSourceContext());
}
$name = $expr->getAttribute('name');
// A bare call to a macro imported via "from" is syntactically a function call;
// it is resolved through the "function" imported symbol registered by
// FromTokenParser, which maps the local alias to the macro name and the
// template it comes from.
if (null !== $alias = $parser->getImportedSymbol('function', $name)) {
$arguments = $this->parseCallableArguments($parser, $line, false, true);
$node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $arguments, $line);
$node->setHasCallParentheses(true);
return $node;
}
$args = $this->parseNamedArguments($parser, false);
$function = $parser->getFunction($name, $line);
if ($function->getParserCallable()) {
$fakeNode = new EmptyNode($line);
$fakeNode->setSourceContext($parser->getStream()->getSourceContext());
$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()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
return new $class($ready ? $function : $function->getName(), $args, $line);
}
public function getName(): string
{
return '(';
}
public function getDescription(): string
{
return 'Twig function call';
}
public function getPrecedence(): int
{
return 512;
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
}
}
@@ -0,0 +1,91 @@
<?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\ExpressionParser\Infix;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Nodes;
use Twig\Parser;
use Twig\Token;
use Twig\TwigTest;
/**
* @internal
*/
class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
private $readyNodes = [];
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
$stream = $parser->getStream();
$test = $parser->getTest($token->getLine());
$arguments = null;
if ($stream->test(Token::OPERATOR_TYPE, '(')) {
$arguments = $this->parseNamedArguments($parser);
} elseif ($test->hasOneMandatoryArgument()) {
$arguments = new Nodes([0 => $parser->parseExpression($this->getPrecedence())]);
}
if ('defined' === $test->getName()) {
if ($expr instanceof NameExpression && null !== $alias = $parser->getImportedSymbol('function', $expr->getAttribute('name'))) {
$expr = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $expr->getTemplateLine()), $expr->getTemplateLine());
$expr->setHasCallParentheses(false);
}
if ($expr instanceof MacroReferenceExpression && $expr->hasCallParentheses()) {
trigger_deprecation('twig/twig', '3.29', 'Using parentheses when testing a macro with the "defined" test is deprecated and will throw a SyntaxError in Twig 4.0; remove the parentheses after the macro name in "%s" at line %d.', $stream->getSourceContext()->getName(), $expr->getTemplateLine());
}
}
$ready = $test instanceof TwigTest;
if (!isset($this->readyNodes[$class = $test->getNodeClass()])) {
$this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
}
if (!$ready = $this->readyNodes[$class]) {
trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
}
return new $class($expr, $ready ? $test : $test->getName(), $arguments, $stream->getCurrent()->getLine());
}
public function getPrecedence(): int
{
return 100;
}
public function getName(): string
{
return 'is';
}
public function getDescription(): string
{
return 'Twig tests';
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
}
}
@@ -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\ExpressionParser\Infix;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
final class IsNotExpressionParser extends IsExpressionParser
{
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
return new NotUnary(parent::parse($parser, $expr, $token), $token->getLine());
}
public function getName(): string
{
return 'is not';
}
}
@@ -0,0 +1,91 @@
<?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\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Nodes;
use Twig\Parser;
use Twig\Template;
use Twig\Token;
/**
* @internal
*/
final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
$stream = $parser->getStream();
$lineno = $token->getLine();
$arguments = new ArrayExpression([], $lineno);
// slice?
$slice = false;
if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
$attribute = new ConstantExpression(0, $token->getLine());
} else {
$attribute = $parser->parseExpression();
}
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
}
if ($slice) {
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
$length = new ConstantExpression(null, $token->getLine());
} else {
$length = $parser->parseExpression();
}
$filter = $parser->getFilter('slice', $token->getLine());
$arguments = new Nodes([$attribute, $length]);
$filter = new ($filter->getNodeClass())($expr, $filter, $arguments, $token->getLine());
$stream->expect(Token::PUNCTUATION_TYPE, ']');
return $filter;
}
$stream->expect(Token::PUNCTUATION_TYPE, ']');
return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno);
}
public function getName(): string
{
return '[';
}
public function getDescription(): string
{
return 'Array access';
}
public function getPrecedence(): int
{
return 512;
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
}
}
@@ -0,0 +1,18 @@
<?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\ExpressionParser;
enum InfixAssociativity
{
case Left;
case Right;
}
@@ -0,0 +1,27 @@
<?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\ExpressionParser;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Parser;
use Twig\Token;
interface InfixExpressionParserInterface extends ExpressionParserInterface
{
/**
* @throws SyntaxError
*/
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression;
public function getAssociativity(): InfixAssociativity;
}
@@ -0,0 +1,42 @@
<?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\ExpressionParser;
/**
* Represents a precedence change.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class PrecedenceChange
{
public function __construct(
private string $package,
private string $version,
private int $newPrecedence,
) {
}
public function getPackage(): string
{
return $this->package;
}
public function getVersion(): string
{
return $this->version;
}
public function getNewPrecedence(): int
{
return $this->newPrecedence;
}
}
@@ -0,0 +1,88 @@
<?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\ExpressionParser\Prefix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
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;
/**
* @internal
*/
final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, Token $token): AbstractExpression
{
$stream = $parser->getStream();
$expr = $parser->parseExpression($this->getPrecedence());
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) {
if (!$stream->test(Token::OPERATOR_TYPE, '=>')) {
return $expr->setExplicitParentheses();
}
return new ListExpression([self::toAssignContextVariable($expr)], $token->getLine());
}
// determine if we are parsing an arrow function arguments
if (!$stream->test(Token::PUNCTUATION_TYPE, ',')) {
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
}
$names = [$expr];
while (true) {
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) {
break;
}
$stream->expect(Token::PUNCTUATION_TYPE, ',');
$token = $stream->expect(Token::NAME_TYPE);
$names[] = new ContextVariable($token->getValue(), $token->getLine());
}
if (!$stream->test(Token::OPERATOR_TYPE, '=>')) {
throw new SyntaxError('A list of variables must be followed by an arrow.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
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
{
return '(';
}
public function getDescription(): string
{
return 'Explicit group expression (a)';
}
public function getPrecedence(): int
{
return 0;
}
}
@@ -0,0 +1,233 @@
<?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\ExpressionParser\Prefix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\PrefixExpressionParserInterface;
use Twig\Lexer;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, Token $token): AbstractExpression
{
$stream = $parser->getStream();
switch (true) {
case $token->test(Token::NAME_TYPE):
$stream->next();
switch ($token->getValue()) {
case 'true':
case 'TRUE':
return new ConstantExpression(true, $token->getLine());
case 'false':
case 'FALSE':
return new ConstantExpression(false, $token->getLine());
case 'none':
case 'NONE':
case 'null':
case 'NULL':
return new ConstantExpression(null, $token->getLine());
default:
return new ContextVariable($token->getValue(), $token->getLine());
}
// no break
case $token->test(Token::NUMBER_TYPE):
$stream->next();
return new ConstantExpression($token->getValue(), $token->getLine());
case $token->test(Token::STRING_TYPE):
case $token->test(Token::INTERPOLATION_START_TYPE):
return $this->parseStringExpression($parser);
case $token->test(Token::PUNCTUATION_TYPE):
// In 4.0, we should always return the node or throw an error for default
if ($node = match ($token->getValue()) {
'{' => $this->parseMappingExpression($parser),
default => null,
}) {
return $node;
}
// no break
case $token->test(Token::OPERATOR_TYPE):
if ('[' === $token->getValue()) {
return $this->parseSequenceExpression($parser);
}
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
// in this context, string operators are variable names
$stream->next();
return new ContextVariable($token->getValue(), $token->getLine());
}
// no break
default:
throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $stream->getSourceContext());
}
}
public function getName(): string
{
return 'literal';
}
public function getOperatorTokens(): array
{
return [];
}
public function getDescription(): string
{
return 'A literal value (boolean, string, number, sequence, mapping, ...)';
}
public function getPrecedence(): int
{
// not used
return 0;
}
private function parseStringExpression(Parser $parser)
{
$stream = $parser->getStream();
$nodes = [];
// a string cannot be followed by another string in a single expression
$nextCanBeString = true;
while (true) {
if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
$nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
$nextCanBeString = false;
} elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
$nodes[] = $parser->parseExpression();
$stream->expect(Token::INTERPOLATION_END_TYPE);
$nextCanBeString = true;
} else {
break;
}
}
$expr = array_shift($nodes);
foreach ($nodes as $node) {
$expr = new ConcatBinary($expr, $node, $node->getTemplateLine());
}
return $expr;
}
private function parseSequenceExpression(Parser $parser)
{
$stream = $parser->getStream();
$stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma');
// trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
break;
}
}
$first = false;
// Check for empty slots (comma with no expression)
if ($stream->test(Token::PUNCTUATION_TYPE, ',')) {
$node->addElement(new EmptyExpression($stream->getCurrent()->getLine()));
} else {
$node->addElement($parser->parseExpression());
}
}
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
return $node;
}
private function parseMappingExpression(Parser $parser)
{
$stream = $parser->getStream();
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma');
// trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
break;
}
}
$first = false;
if ($stream->test(Token::OPERATOR_TYPE, '...')) {
$node->addElement($parser->parseExpression());
continue;
}
// a mapping key can be:
//
// * a number -- 12
// * a string -- 'a'
// * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2)
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$key = new ConstantExpression($token->getValue(), $token->getLine());
// {a} is a shortcut for {a:a}
if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) {
$value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine());
$node->addElement($value, $key);
continue;
}
} elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) {
$key = new ConstantExpression($token->getValue(), $token->getLine());
} elseif ($stream->test(Token::OPERATOR_TYPE, '(')) {
$key = $parser->parseExpression();
} else {
$current = $stream->getCurrent();
throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext());
}
$stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)');
$value = $parser->parseExpression();
$node->addElement($value, $key);
}
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
return $node;
}
}
@@ -0,0 +1,72 @@
<?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\ExpressionParser\Prefix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\ExpressionParser\PrefixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
final class UnaryOperatorExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function __construct(
/** @var class-string<AbstractUnary> */
private string $nodeClass,
private string $name,
private int $precedence,
private ?PrecedenceChange $precedenceChange = null,
private ?string $description = null,
private array $aliases = [],
private ?int $operandPrecedence = null,
) {
}
/**
* @return AbstractUnary
*/
public function parse(Parser $parser, Token $token): AbstractExpression
{
return new ($this->nodeClass)($parser->parseExpression($this->operandPrecedence ?? $this->precedence), $token->getLine());
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description ?? '';
}
public function getPrecedence(): int
{
return $this->precedence;
}
public function getPrecedenceChange(): ?PrecedenceChange
{
return $this->precedenceChange;
}
public function getAliases(): array
{
return $this->aliases;
}
}
@@ -0,0 +1,25 @@
<?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\ExpressionParser;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Parser;
use Twig\Token;
interface PrefixExpressionParserInterface extends ExpressionParserInterface
{
/**
* @throws SyntaxError
*/
public function parse(Parser $parser, Token $token): AbstractExpression;
}
@@ -11,7 +11,7 @@
namespace Twig\Extension;
abstract class AbstractExtension implements ExtensionInterface
abstract class AbstractExtension implements LastModifiedExtensionInterface
{
public function getTokenParsers()
{
@@ -42,4 +42,26 @@ abstract class AbstractExtension implements ExtensionInterface
{
return [[], []];
}
public function getExpressionParsers(): array
{
return [];
}
public function getLastModified(): int
{
$filename = (new \ReflectionClass($this))->getFileName();
if (!is_file($filename)) {
return 0;
}
$lastModified = filemtime($filename);
// Track modifications of the runtime class if it exists and follows the naming convention
if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13).'Runtime.php')) {
$lastModified = max($lastModified, filemtime($filename));
}
return $lastModified;
}
}
@@ -0,0 +1,179 @@
<?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\Extension;
use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;
use Twig\Attribute\AsTwigTest;
use Twig\Environment;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/**
* Define Twig filters, functions, and tests with PHP attributes.
*
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
final class AttributeExtension extends AbstractExtension
{
private array $filters;
private array $functions;
private array $tests;
/**
* Use a runtime class using PHP attributes to define filters, functions, and tests.
*
* @param class-string $class
*/
public function __construct(private string $class)
{
}
/**
* @return class-string
*/
public function getClass(): string
{
return $this->class;
}
public function getFilters(): array
{
if (!isset($this->filters)) {
$this->initFromAttributes();
}
return $this->filters;
}
public function getFunctions(): array
{
if (!isset($this->functions)) {
$this->initFromAttributes();
}
return $this->functions;
}
public function getTests(): array
{
if (!isset($this->tests)) {
$this->initFromAttributes();
}
return $this->tests;
}
public function getLastModified(): int
{
return max(
filemtime(__FILE__),
is_file($filename = (new \ReflectionClass($this->getClass()))->getFileName()) ? filemtime($filename) : 0,
);
}
private function initFromAttributes(): void
{
$filters = $functions = $tests = [];
$reflectionClass = new \ReflectionClass($this->getClass());
foreach ($reflectionClass->getMethods() as $method) {
foreach ($method->getAttributes(AsTwigFilter::class) as $reflectionAttribute) {
/** @var AsTwigFilter $attribute */
$attribute = $reflectionAttribute->newInstance();
$callable = new TwigFilter($attribute->name, [$reflectionClass->name, $method->getName()], [
'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,
]);
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
}
$filters[$attribute->name] = $callable;
}
foreach ($method->getAttributes(AsTwigFunction::class) as $reflectionAttribute) {
/** @var AsTwigFunction $attribute */
$attribute = $reflectionAttribute->newInstance();
$callable = new TwigFunction($attribute->name, [$reflectionClass->name, $method->getName()], [
'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,
]);
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
}
$functions[$attribute->name] = $callable;
}
foreach ($method->getAttributes(AsTwigTest::class) as $reflectionAttribute) {
/** @var AsTwigTest $attribute */
$attribute = $reflectionAttribute->newInstance();
$callable = new TwigTest($attribute->name, [$reflectionClass->name, $method->getName()], [
'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,
]);
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
}
$tests[$attribute->name] = $callable;
}
}
// Assign all at the end to avoid inconsistent state in case of exception
$this->filters = array_values($filters);
$this->functions = array_values($functions);
$this->tests = array_values($tests);
}
/**
* Detect if the first argument of the method is the environment.
*/
private function needsEnvironment(\ReflectionFunctionAbstract $function): bool
{
if (!$parameters = $function->getParameters()) {
return false;
}
return $parameters[0]->getType() instanceof \ReflectionNamedType
&& Environment::class === $parameters[0]->getType()->getName()
&& !$parameters[0]->isVariadic();
}
}
File diff suppressed because it is too large. Load diff
@@ -14,6 +14,7 @@ namespace Twig\Extension;
use Twig\Environment;
use Twig\FileExtensionEscapingStrategy;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Filter\EscapeFilter;
use Twig\Node\Expression\Filter\RawFilter;
use Twig\Node\Node;
use Twig\NodeVisitor\EscaperNodeVisitor;
@@ -51,12 +52,20 @@ final class EscaperExtension extends AbstractExtension
public function getFilters(): array
{
return [
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe'], 'node_class' => EscapeFilter::class]),
new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe'], 'node_class' => EscapeFilter::class]),
new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),
];
}
public function getLastModified(): int
{
return max(
parent::getLastModified(),
filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()),
);
}
/**
* @deprecated since Twig 3.10
*/
@@ -68,13 +77,13 @@ final class EscaperExtension extends AbstractExtension
}
$this->environment = $environment;
$this->escaper = $environment->getRuntime(EscaperRuntime::class);
$this->escaper = null;
}
/**
* @deprecated since Twig 3.10
*/
public function setEscaperRuntime(EscaperRuntime $escaper)
public function setEscaperRuntime(EscaperRuntime $escaper): void
{
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
@@ -124,20 +133,18 @@ final class EscaperExtension extends AbstractExtension
*
* @deprecated since Twig 3.10
*/
public function setEscaper($strategy, callable $callable)
public function setEscaper($strategy, callable $callable): void
{
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);
}
/**
@@ -157,33 +164,40 @@ final class EscaperExtension extends AbstractExtension
/**
* @deprecated since Twig 3.10
*/
public function setSafeClasses(array $safeClasses = [])
public function setSafeClasses(array $safeClasses = []): void
{
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);
}
/**
* @deprecated since Twig 3.10
*/
public function addSafeClass(string $class, array $strategies)
public function addSafeClass(string $class, array $strategies): void
{
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));
}
/**
* @internal
*
* @return array<string>
*/
public static function escapeFilterIsSafe(Node $filterArgs)
{
@@ -12,7 +12,10 @@
namespace Twig\Extension;
use Twig\ExpressionParser;
use Twig\Node\Expression\AbstractExpression;
use Twig\ExpressionParser\ExpressionParserInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
@@ -23,6 +26,8 @@ use Twig\TwigTest;
* Interface implemented by extension classes.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @method array<ExpressionParserInterface> getExpressionParsers()
*/
interface ExtensionInterface
{
@@ -64,11 +69,11 @@ interface ExtensionInterface
/**
* Returns a list of operators to add to the existing list.
*
* @return array<array> First array of unary operators, second array of binary operators
* @return array<array>
*
* @psalm-return array{
* array<string, array{precedence: int, class: class-string<AbstractExpression>}>,
* array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
* array<string, array{precedence: int, precedence_change?: PrecedenceChange, class: class-string<AbstractUnary>}>,
* array<string, array{precedence: int, precedence_change?: PrecedenceChange, class?: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
* }
*/
public function getOperators();
@@ -0,0 +1,23 @@
<?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\Extension;
interface LastModifiedExtensionInterface extends ExtensionInterface
{
/**
* Returns the last modification time of the extension for cache invalidation.
*
* This timestamp should be the last time the source code of the extension class
* and all its dependencies were modified (including the Runtime class).
*/
public function getLastModified(): int;
}
@@ -0,0 +1,39 @@
<?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\Extension;
use Twig\Node\Expression\FunctionNode\RenderSandboxedFunction;
use Twig\Runtime\SandboxBridgeRuntime;
use Twig\TwigFunction;
/**
* Exposes a sandbox to trusted templates.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class SandboxBridgeExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('render_sandboxed', [SandboxBridgeRuntime::class, 'render'], ['is_safe_callback' => [RenderSandboxedFunction::class, 'getSafe'], 'node_class' => RenderSandboxedFunction::class]),
];
}
public function getLastModified(): int
{
return max(
parent::getLastModified(),
filemtime((new \ReflectionClass(SandboxBridgeRuntime::class))->getFileName()),
);
}
}
@@ -12,25 +12,31 @@
namespace Twig\Extension;
use Twig\NodeVisitor\SandboxNodeVisitor;
use Twig\Sandbox\Sandbox;
use Twig\Sandbox\SecurityChecker;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Sandbox\SourcePolicyInterface;
use Twig\Source;
use Twig\TokenParser\SandboxTokenParser;
/**
* This extension is the wiring behind "Twig\Sandbox\Sandbox" and should not
* be used directly, use a "Sandbox" to render untrusted templates instead.
*
* @internal since Twig 3.29
*/
final class SandboxExtension extends AbstractExtension
{
private $sandboxedGlobally;
private $sandboxed;
private $policy;
private $sourcePolicy;
private SecurityChecker $checker;
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null)
{
$this->policy = $policy;
$this->sandboxedGlobally = $sandboxed;
$this->sourcePolicy = $sourcePolicy;
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->checker = new SecurityChecker($policy, (bool) $sandboxed, $sourcePolicy);
}
public function getTokenParsers(): array
@@ -43,93 +49,99 @@ final class SandboxExtension extends AbstractExtension
return [new SandboxNodeVisitor()];
}
public function enableSandbox(): void
/**
* @internal
*/
public function getChecker(): SecurityChecker
{
$this->sandboxed = true;
return $this->checker;
}
/**
* @deprecated since Twig 3.29, use "Twig\Sandbox\Sandbox" to render untrusted templates instead
*/
public function enableSandbox(): void
{
trigger_deprecation('twig/twig', '3.29', 'The "%s()" method is deprecated, use "%s" to render untrusted templates instead.', __METHOD__, Sandbox::class);
$this->checker->setSandboxed(true);
}
/**
* @deprecated since Twig 3.29, use "Twig\Sandbox\Sandbox" to render untrusted templates instead
*/
public function disableSandbox(): void
{
$this->sandboxed = false;
trigger_deprecation('twig/twig', '3.29', 'The "%s()" method is deprecated, use "%s" to render untrusted templates instead.', __METHOD__, Sandbox::class);
$this->checker->setSandboxed(false);
}
public function isSandboxed(?Source $source = null): bool
{
return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
return $this->checker->isSandboxed($source);
}
/**
* @deprecated since Twig 3.29, use "Twig\Sandbox\Sandbox" to render untrusted templates instead
*/
public function isSandboxedGlobally(): bool
{
return $this->sandboxedGlobally;
trigger_deprecation('twig/twig', '3.29', 'The "%s()" method is deprecated, use "%s" to render untrusted templates instead.', __METHOD__, Sandbox::class);
return $this->checker->isSandboxedGlobally();
}
private function isSourceSandboxed(?Source $source): bool
public function setSecurityPolicy(SecurityPolicyInterface $policy): void
{
if (null === $source || null === $this->sourcePolicy) {
return false;
}
return $this->sourcePolicy->enableSandbox($source);
}
public function setSecurityPolicy(SecurityPolicyInterface $policy)
{
$this->policy = $policy;
$this->checker->setSecurityPolicy($policy);
}
public function getSecurityPolicy(): SecurityPolicyInterface
{
return $this->policy;
return $this->checker->getSecurityPolicy();
}
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 = [];
}
$this->checker->checkSecurity($tags, $filters, $functions, $tests, $source);
}
public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void
{
if ($this->isSandboxed($source)) {
try {
$this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
}
$this->checker->checkMethodAllowed($obj, $method, $lineno, $source);
}
public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void
{
if ($this->isSandboxed($source)) {
try {
$this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
}
$this->checker->checkPropertyAllowed($obj, $property, $lineno, $source);
}
/**
* @throws SecurityNotAllowedMethodError
*/
public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
{
if ($this->isSandboxed($source) && $obj instanceof \Stringable) {
try {
$this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
return $this->checker->ensureToStringAllowed($obj, $lineno, $source);
}
throw $e;
}
}
return $obj;
/**
* 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
{
return $this->checker->ensureSpreadAllowed($obj, $lineno, $source);
}
}
@@ -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
+113 -37
View File
@@ -12,15 +12,25 @@
namespace Twig;
use Twig\Error\RuntimeError;
use Twig\ExpressionParser\ExpressionParsers;
use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
use Twig\Extension\AttributeExtension;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\LastModifiedExtensionInterface;
use Twig\Extension\StagingExtension;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
// Help opcache.preload discover always-needed symbols
// @see https://github.com/php/php-src/issues/10131
class_exists(BinaryOperatorExpressionParser::class);
/**
* @author Fabien Potencier <fabien@symfony.com>
*
@@ -46,14 +56,16 @@ final class ExtensionSet
private $functions;
/** @var array<string, TwigFunction> */
private $dynamicFunctions;
/** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
private $unaryOperators;
/** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
private $binaryOperators;
/** @var array<string, mixed> */
private ExpressionParsers $expressionParsers;
/** @var array<string, mixed>|null */
private $globals;
/** @var array<callable(string): (TwigFunction|false)> */
private $functionCallbacks = [];
/** @var array<callable(string): (TwigFilter|false)> */
private $filterCallbacks = [];
/** @var array<callable(string): (TwigTest|false)> */
private $testCallbacks = [];
/** @var array<callable(string): (TokenParserInterface|false)> */
private $parserCallbacks = [];
private $lastModified = 0;
@@ -62,7 +74,7 @@ final class ExtensionSet
$this->staging = new StagingExtension();
}
public function initRuntime()
public function initRuntime(): void
{
$this->runtimeInitialized = true;
}
@@ -117,19 +129,28 @@ final class ExtensionSet
return $this->lastModified;
}
$lastModified = 0;
foreach ($this->extensions as $extension) {
$r = new \ReflectionObject($extension);
if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) {
$this->lastModified = $extensionTime;
if ($extension instanceof LastModifiedExtensionInterface) {
$lastModified = max($extension->getLastModified(), $lastModified);
} else {
$r = new \ReflectionObject($extension);
if (is_file($r->getFileName())) {
$lastModified = max(filemtime($r->getFileName()), $lastModified);
}
}
}
return $this->lastModified;
return $this->lastModified = $lastModified;
}
public function addExtension(ExtensionInterface $extension): void
{
$class = \get_class($extension);
if ($extension instanceof AttributeExtension) {
$class = $extension->getClass();
} else {
$class = $extension::class;
}
if ($this->initialized) {
throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
@@ -190,6 +211,9 @@ final class ExtensionSet
return null;
}
/**
* @param callable(string): (TwigFunction|false) $callable
*/
public function registerUndefinedFunctionCallback(callable $callable): void
{
$this->functionCallbacks[] = $callable;
@@ -243,6 +267,9 @@ final class ExtensionSet
return null;
}
/**
* @param callable(string): (TwigFilter|false) $callable
*/
public function registerUndefinedFilterCallback(callable $callable): void
{
$this->filterCallbacks[] = $callable;
@@ -309,6 +336,9 @@ final class ExtensionSet
return null;
}
/**
* @param callable(string): (TokenParserInterface|false) $callable
*/
public function registerUndefinedTokenParserCallback(callable $callable): void
{
$this->parserCallbacks[] = $callable;
@@ -383,31 +413,30 @@ final class ExtensionSet
}
}
foreach ($this->testCallbacks as $callback) {
if (false !== $test = $callback($name)) {
return $test;
}
}
return null;
}
/**
* @return array<string, array{precedence: int, class: class-string<AbstractExpression>}>
* @param callable(string): (TwigTest|false) $callable
*/
public function getUnaryOperators(): array
public function registerUndefinedTestCallback(callable $callable): void
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->unaryOperators;
$this->testCallbacks[] = $callable;
}
/**
* @return array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
public function getExpressionParsers(): ExpressionParsers
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->binaryOperators;
return $this->expressionParsers;
}
private function initExtensions(): void
@@ -420,8 +449,7 @@ final class ExtensionSet
$this->dynamicFunctions = [];
$this->dynamicTests = [];
$this->visitors = [];
$this->unaryOperators = [];
$this->binaryOperators = [];
$this->expressionParsers = new ExpressionParsers();
foreach ($this->extensions as $extension) {
$this->initExtension($extension);
@@ -471,18 +499,66 @@ final class ExtensionSet
$this->visitors[] = $visitor;
}
// operators
if ($operators = $extension->getOperators()) {
if (!\is_array($operators)) {
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
}
// expression parsers
if (method_exists($extension, 'getExpressionParsers')) {
$this->expressionParsers->add($extension->getExpressionParsers());
}
if (2 !== \count($operators)) {
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
}
$operators = $extension->getOperators();
if (!\is_array($operators)) {
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', $extension::class, get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators)));
}
$this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
$this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
if (2 !== \count($operators)) {
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', $extension::class, \count($operators)));
}
$expressionParsers = [];
foreach ($operators[0] as $operator => $op) {
$expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []);
}
foreach ($operators[1] as $operator => $op) {
$op['associativity'] = match ($op['associativity']) {
1 => InfixAssociativity::Left,
2 => InfixAssociativity::Right,
default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $op['associativity'], $operator)),
};
if (isset($op['callable'])) {
$expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']);
} else {
$expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []);
}
}
if (\count($expressionParsers)) {
trigger_deprecation('twig/twig', '3.21', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', $extension::class));
$this->expressionParsers->add($expressionParsers);
}
}
private function convertInfixExpressionParser(string $nodeClass, string $operator, int $precedence, InfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface
{
trigger_deprecation('twig/twig', '3.21', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator));
return new class($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases, $callable) extends BinaryOperatorExpressionParser {
public function __construct(
string $nodeClass,
string $operator,
int $precedence,
InfixAssociativity $associativity = InfixAssociativity::Left,
?PrecedenceChange $precedenceChange = null,
array $aliases = [],
private $callable = null,
) {
parent::__construct($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases);
}
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
return ($this->callable)($parser, $expr);
}
};
}
}
@@ -33,7 +33,7 @@ class FileExtensionEscapingStrategy
*/
public static function guess(string $name)
{
if (\in_array(substr($name, -1), ['/', '\\'])) {
if (\in_array(substr($name, -1), ['/', '\\'], true)) {
return 'html'; // return html for directories
}
@@ -45,6 +45,7 @@ class FileExtensionEscapingStrategy
switch ($extension) {
case 'js':
case 'json':
return 'js';
case 'css':
+176 -81
View File
@@ -13,6 +13,7 @@
namespace Twig;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\ExpressionParsers;
/**
* @author Fabien Potencier <fabien@symfony.com>
@@ -36,6 +37,10 @@ class Lexer
private $position;
private $positions;
private $currentVarBlockLine;
/** @var list<string> */
private array $documentation = [];
private array $openingBrackets = ['{', '(', '['];
private array $closingBrackets = ['}', ')', ']'];
public const STATE_DATA = 0;
public const STATE_BLOCK = 1;
@@ -44,12 +49,23 @@ class Lexer
public const STATE_INTERPOLATION = 4;
public const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A';
public const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A';
public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
public const REGEX_NUMBER = '/(?(DEFINE)
(?<LNUM>[0-9]+(_[0-9]+)*) # Integers (with underscores) 123_456
(?<FRAC>\.(?&LNUM)) # Fractional part .456
(?<EXPONENT>[eE][+-]?(?&LNUM)) # Exponent part E+10
(?<DNUM>(?&LNUM)(?:(?&FRAC))?) # Decimal number 123_456.456
)(?:(?&DNUM)(?:(?&EXPONENT))?) # 123_456.456E+10
/Ax';
public const REGEX_DQ_STRING_DELIM = '/"/A';
public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
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",
@@ -73,12 +89,16 @@ class Lexer
], $options);
}
private function initialize()
private function initialize(): void
{
if ($this->isInitialized) {
return;
}
// when the comment closing tag starts with "#" (as "#}" does), the empty comment ("{##}")
// starts like a documentation comment ("{##"); make sure it keeps lexing as a comment
$emptyCommentLookahead = str_starts_with($this->options['tag_comment'][1], '#') ? '(?!'.preg_quote(substr($this->options['tag_comment'][1], 1), '#').')' : '';
// when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default
$this->regexes = [
// }}
@@ -101,7 +121,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',
@@ -131,7 +151,22 @@ 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',
// #} or ##}
'lex_documentation_comment' => '{
(?:'.
preg_quote($this->options['whitespace_trim'].'#'.$this->options['tag_comment'][1], '#').'\s*\n?'. // -##}\s*\n?
'|'.
preg_quote($this->options['whitespace_line_trim'].'#'.$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~##}[ \t\0\x0B]*
'|'.
preg_quote($this->options['whitespace_trim'].$this->options['tag_comment'][1], '#').'\s*\n?'. // -#}\s*\n?
'|'.
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], '#').'(?:\r\n?|\n)?'. // #}(?:\r\n?|\n)?
')
}sx',
@@ -156,6 +191,8 @@ class Lexer
'|'.
preg_quote($this->options['tag_block'][0], '#'). // {%
'|'.
preg_quote($this->options['tag_comment'][0].'#', '#').$emptyCommentLookahead. // {## (but not the empty comment {##})
'|'.
preg_quote($this->options['tag_comment'][0], '#'). // {#
')('.
preg_quote($this->options['whitespace_trim'], '#'). // -
@@ -175,7 +212,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);
@@ -184,6 +221,7 @@ class Lexer
$this->states = [];
$this->brackets = [];
$this->position = -1;
$this->documentation = [];
// find all token starts in one go
preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, \PREG_OFFSET_CAPTURE);
@@ -217,7 +255,7 @@ class Lexer
$this->pushToken(Token::EOF_TYPE);
if (!empty($this->brackets)) {
if ($this->brackets) {
[$expect, $lineno] = array_pop($this->brackets);
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
@@ -229,8 +267,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;
}
@@ -258,32 +297,47 @@ 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(true);
break;
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->documentation = [];
$this->moveCursor($match[0]);
$this->lexRawData();
// {% line \d+ %}
} elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) {
$this->documentation = [];
$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;
@@ -292,9 +346,8 @@ class Lexer
private function lexBlock(): void
{
if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::BLOCK_END_TYPE);
$this->moveCursor($match[0]);
if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
$this->pushClosingToken(Token::BLOCK_END_TYPE, $match[0]);
$this->popState();
} else {
$this->lexExpression();
@@ -303,9 +356,8 @@ class Lexer
private function lexVar(): void
{
if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::VAR_END_TYPE);
$this->moveCursor($match[0]);
if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
$this->pushClosingToken(Token::VAR_END_TYPE, $match[0]);
$this->popState();
} else {
$this->lexExpression();
@@ -323,19 +375,13 @@ class Lexer
}
}
// spread operator
if ('.' === $this->code[$this->cursor] && ($this->cursor + 2 < $this->end) && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) {
$this->pushToken(Token::SPREAD_TYPE, '...');
$this->moveCursor('...');
}
// arrow function
elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) {
$this->pushToken(Token::ARROW_TYPE, '=>');
$this->moveCursor('=>');
}
// operators
elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0]));
if (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
$operator = preg_replace('/\s+/', ' ', $match[0]);
if (\in_array($operator, $this->openingBrackets, true)) {
$this->checkBrackets($operator);
}
$this->pushToken(Token::OPERATOR_TYPE, $operator);
$this->moveCursor($match[0]);
}
// names
@@ -345,37 +391,18 @@ class Lexer
}
// numbers
elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) {
$number = (float) $match[0]; // floats
if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
$number = (int) $match[0]; // integers lower than the maximum
}
$this->pushToken(Token::NUMBER_TYPE, $number);
$this->pushToken(Token::NUMBER_TYPE, 0 + str_replace('_', '', $match[0]));
$this->moveCursor($match[0]);
}
// punctuation
elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) {
// opening bracket
if (str_contains('([{', $this->code[$this->cursor])) {
$this->brackets[] = [$this->code[$this->cursor], $this->lineno];
}
// closing bracket
elseif (str_contains(')]}', $this->code[$this->cursor])) {
if (empty($this->brackets)) {
throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
}
[$expect, $lineno] = array_pop($this->brackets);
if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) {
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
}
$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
@@ -384,9 +411,16 @@ class Lexer
$this->pushState(self::STATE_STRING);
$this->moveCursor($match[0]);
}
// inline comment
elseif (preg_match(self::REGEX_RAW_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
if (str_starts_with($match[0], '##')) {
$this->addDocumentation(substr($match[0], 2));
}
$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));
}
}
@@ -418,7 +452,7 @@ class Lexer
$result .= $nextChar;
} elseif ("'" === $nextChar || '"' === $nextChar) {
if ($nextChar !== $quoteType) {
trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
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;
} elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) {
@@ -435,9 +469,9 @@ 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" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
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;
}
@@ -453,6 +487,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]);
@@ -468,16 +503,27 @@ class Lexer
}
}
$this->pushToken(Token::TEXT_TYPE, $text);
$this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text), $offset);
}
private function lexComment(): void
private function lexComment(bool $isDocumentation = false): void
{
if (!preg_match($this->regexes['lex_comment'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
$regex = $isDocumentation ? 'lex_documentation_comment' : 'lex_comment';
if (!preg_match($this->regexes[$regex], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
throw new SyntaxError('Unclosed comment.', $this->lineno, $this->source);
}
$this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]);
$comment = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
if ($isDocumentation) {
$documentation = $comment;
// support a symmetric "##}" closing marker
if (str_ends_with($documentation, '#')) {
$documentation = substr($documentation, 0, -1);
}
$this->addDocumentation($documentation);
}
$this->moveCursor($comment.$match[0][0]);
}
private function lexString(): void
@@ -488,7 +534,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);
@@ -497,10 +543,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));
}
}
@@ -509,53 +555,84 @@ 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
$documentation = null;
if ($this->documentation && (Token::TEXT_TYPE !== $type || '' !== trim($value))) {
$documentation = implode("\n", $this->documentation);
$this->documentation = [];
}
$this->tokens[] = new Token($type, $value, $lineno ?? $this->lineno, $offset ?? $this->cursor, $documentation);
}
private function addDocumentation(string $documentation): void
{
if ('' !== $documentation = trim($this->normalizeNewlines($documentation))) {
$this->documentation[] = $documentation;
}
}
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
{
$operators = array_merge(
['='],
array_keys($this->env->getUnaryOperators()),
array_keys($this->env->getBinaryOperators())
);
$expressionParsers = [];
foreach ($this->env->getExpressionParsers() as $expressionParser) {
$expressionParsers = array_merge($expressionParsers, ExpressionParsers::getOperatorTokensFor($expressionParser));
}
$operators = array_combine($operators, array_map('strlen', $operators));
arsort($operators);
$expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers));
arsort($expressionParsers);
$regex = [];
foreach ($operators as $operator => $length) {
foreach ($expressionParsers as $expressionParser => $length) {
// an operator that ends with a character must be followed by
// a whitespace, a parenthesis, an opening map [ or sequence {
$r = preg_quote($operator, '/');
if (ctype_alpha($operator[$length - 1])) {
$r = preg_quote($expressionParser, '/');
if (ctype_alpha($expressionParser[$length - 1])) {
$r .= '(?=[\s()\[{])';
}
// an operator that begins with a character must not have a dot or pipe before
if (ctype_alpha($operator[0])) {
$r = '(?<![\.\|])'.$r;
if (ctype_alpha($expressionParser[0])) {
$r = '(?<![\.\|]\s|.[\.\|])'.$r;
}
// an operator with a space can be any amount of whitespaces
@@ -581,4 +658,22 @@ class Lexer
$this->state = array_pop($this->states);
}
private function checkBrackets(string $code): void
{
// opening bracket
if (\in_array($code, $this->openingBrackets, true)) {
$this->brackets[] = [$code, $this->lineno];
} elseif (\in_array($code, $this->closingBrackets, true)) {
// closing bracket
if (!$this->brackets) {
throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor));
}
[$expect, $lineno] = array_pop($this->brackets);
if ($code !== str_replace($this->openingBrackets, $this->closingBrackets, $expect)) {
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
}
}
}
@@ -104,7 +104,7 @@ final class ChainLoader implements LoaderInterface
try {
return $loader->getCacheKey($name);
} catch (LoaderError $e) {
$exceptions[] = \get_class($loader).': '.$e->getMessage();
$exceptions[] = $loader::class.': '.$e->getMessage();
}
}
@@ -123,7 +123,7 @@ final class ChainLoader implements LoaderInterface
try {
return $loader->isFresh($name, $time);
} catch (LoaderError $e) {
$exceptions[] = \get_class($loader).': '.$e->getMessage();
$exceptions[] = $loader::class.': '.$e->getMessage();
}
}
@@ -24,6 +24,9 @@ class FilesystemLoader implements LoaderInterface
/** Identifier of the main namespace. */
public const MAIN_NAMESPACE = '__main__';
/**
* @var array<string, list<string>>
*/
protected $paths = [];
protected $cache = [];
protected $errorCache = [];
@@ -31,8 +34,8 @@ class FilesystemLoader implements LoaderInterface
private $rootPath;
/**
* @param string|array $paths A path or an array of paths where to look for templates
* @param string|null $rootPath The root path common to all relative paths (null for getcwd())
* @param string|string[] $paths A path or an array of paths where to look for templates
* @param string|null $rootPath The root path common to all relative paths (null for getcwd())
*/
public function __construct($paths = [], ?string $rootPath = null)
{
@@ -48,6 +51,8 @@ class FilesystemLoader implements LoaderInterface
/**
* Returns the paths to the templates.
*
* @return list<string>
*/
public function getPaths(string $namespace = self::MAIN_NAMESPACE): array
{
@@ -58,6 +63,8 @@ class FilesystemLoader implements LoaderInterface
* Returns the path namespaces.
*
* The main namespace is always defined.
*
* @return list<string>
*/
public function getNamespaces(): array
{
@@ -65,7 +72,7 @@ class FilesystemLoader implements LoaderInterface
}
/**
* @param string|array $paths A path or an array of paths where to look for templates
* @param string|string[] $paths A path or an array of paths where to look for templates
*/
public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void
{
+136
View File
@@ -0,0 +1,136 @@
<?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;
use Twig\Error\RuntimeError;
use Twig\Node\Expression\MacroReferenceExpression;
/**
* @internal
*/
final class MacroNamespace
{
/**
* @param array<string, TwigMacro> $macros
*/
public function __construct(
private Template $template,
private array $macros,
) {
}
public function has(string $name, array $context): bool
{
if (null !== $declaration = $this->findDeclaredName($name, $context)) {
[$declaredName, $templateName] = $declaration;
if ($declaredName !== $name) {
trigger_deprecation('twig/twig', '3.29', 'Testing whether the macro "%s" (defined in template "%s") is defined as "%s" is deprecated; macro names will be case-sensitive in Twig 4.0 and this test will return false.', $declaredName, $templateName, $name);
}
return true;
}
if (!str_starts_with($name, 'macro_') || null === $this->findDeclaredName($bareName = substr($name, \strlen('macro_')), $context)) {
return false;
}
trigger_deprecation('twig/twig', '3.29', 'Testing whether the macro "%s" is defined via the "macro_"-prefixed name "%s" is deprecated; pass the bare macro name to "%s" instead.', $bareName, $name, MacroReferenceExpression::class);
return true;
}
/**
* @param array<int|string, mixed> $arguments
*/
public function call(string $name, array $arguments, array $context, int $line, Source $source): string|Markup
{
if (null !== $macro = $this->getDeclared($name)) {
return $macro->callLegacy($arguments, $source, $line);
}
if (null === $macro = $this->resolve($name, $context)) {
if (!str_starts_with($name, 'macro_') || null === $macro = $this->resolve($bareName = substr($name, \strlen('macro_')), $context)) {
throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', $name, $this->template->getTemplateName()), $line, $source);
}
trigger_deprecation('twig/twig', '3.29', 'Calling the macro "%s" via the "macro_"-prefixed name "%s" is deprecated; pass the bare macro name to "%s" instead.', $bareName, $name, MacroReferenceExpression::class);
}
return $macro->callLegacy($arguments, $source, $line);
}
/**
* @return array{string, string}|null
*/
private function findDeclaredName(string $name, array $context): ?array
{
$namespace = $this;
while (true) {
if (isset($namespace->macros[$name])) {
return [$name, $namespace->template->getTemplateName()];
}
foreach ($namespace->macros as $declaredName => $macro) {
if (0 === strcasecmp($declaredName, $name)) {
return [$declaredName, $namespace->template->getTemplateName()];
}
}
if (null === $namespace = $namespace->getParent($context)) {
return null;
}
}
}
private function getDeclared(string $name): ?TwigMacro
{
if (isset($this->macros[$name])) {
$this->template->ensureSecurityChecked();
return $this->macros[$name];
}
foreach ($this->macros as $declaredName => $macro) {
if (0 === strcasecmp($declaredName, $name)) {
trigger_deprecation('twig/twig', '3.29', 'Calling the macro "%s" (defined in template "%s") as "%s" is deprecated; macro names will be case-sensitive in Twig 4.0.', $declaredName, $this->template->getTemplateName(), $name);
$this->template->ensureSecurityChecked();
return $macro;
}
}
return null;
}
private function resolve(string $name, array $context): ?TwigMacro
{
$namespace = $this;
while (true) {
if (null !== $macro = $namespace->getDeclared($name)) {
return $macro;
}
if (null === $namespace = $namespace->getParent($context)) {
return null;
}
}
}
private function getParent(array $context): ?self
{
if (!$parent = $this->template->getParent($context)) {
return null;
}
return $parent->unwrap()->getMacroNamespace();
}
}
+16 -1
View File
@@ -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,11 +37,16 @@ class Markup implements \Countable, \JsonSerializable, \Stringable
$this->charset = $charset;
}
public function __toString()
public function __toString(): string
{
return $this->content;
}
public function getCharset(): string
{
return $this->charset;
}
/**
* @return int
*/
+1 -1
View File
@@ -42,7 +42,7 @@ class BlockNode extends Node
$compiler
->subcompile($this->getNode('body'))
->write("yield from [];\n")
->write("return; yield;\n")
->outdent()
->write("}\n\n")
;
+1 -1
View File
@@ -39,7 +39,7 @@ class CaptureNode extends Node
->raw("(function () use (&\$context, \$macros, \$blocks) {\n")
->indent()
->subcompile($this->getNode('body'))
->write("yield from [];\n")
->write("return; yield;\n")
->outdent()
->write('})()')
;
@@ -15,16 +15,21 @@ use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* Wires the security checker at the very top of the template constructor, as
* the constructor resolves `use` traits before the sandbox could check them.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CheckSecurityCallNode extends Node
{
/**
* @return void
*/
public function compile(Compiler $compiler)
{
$compiler
->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n")
->write("\$this->checkSecurity();\n")
->write("\$this->sandbox = \$env->getExtension(SandboxExtension::class)->getChecker();\n")
;
}
}
@@ -23,45 +23,90 @@ 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();
}
public function compile(Compiler $compiler): void
{
if (isset($this->usedTags['use'])) {
$compiler
->write("\n")
->write("protected function checkTraitsAllowed(): void\n")
->write("{\n")
->indent()
->write("if (!\$this->sandbox->isSandboxed(\$this->source)) {\n")
->indent()
->write("return;\n")
->outdent()
->write("}\n\n")
->write("try {\n")
->indent()
->write("\$this->sandbox->checkSecurity(['use'], [], [], [], \$this->source);\n")
->outdent()
->write("} catch (SecurityNotAllowedTagError \$e) {\n")
->indent()
->write('$e->setTemplateLine(')->repr($this->usedTags['use'])->raw(");\n\n")
->write("throw \$e;\n")
->outdent()
->write("}\n")
->outdent()
->write("}\n")
;
}
$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")
->outdent()
->write("} catch (SecurityError \$e) {\n")
->indent()
->write("\$e->setSourceContext(\$this->source);\n\n")
->write("if (\$e instanceof SecurityNotAllowedTagError && isset(\$tags[\$e->getTagName()])) {\n")
->indent()
->write("\$e->setTemplateLine(\$tags[\$e->getTagName()]);\n")
@@ -74,6 +119,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())
@@ -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;
}
@@ -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]
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;
}
}
+9 -5
View File
@@ -33,18 +33,22 @@ class EmbedNode extends IncludeNode
$this->setAttribute('index', $index);
}
protected function addGetTemplate(Compiler $compiler): void
protected function addGetTemplate(Compiler $compiler, string $template = ''): void
{
$compiler
->write('$this->loadTemplate(')
->raw('$this->load(')
->string($this->getAttribute('name'))
->raw(', ')
->repr($this->getTemplateName())
->raw(', ')
->repr($this->getTemplateLine())
->raw(', ')
->string($this->getAttribute('index'))
->repr($this->getAttribute('index'))
->raw(')')
;
if ($this->getAttribute('ignore_missing')) {
$compiler
->raw(";\n")
->write(\sprintf("\$%s->getParent(\$context);\n", $template))
;
}
}
}
@@ -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\Node;
use Twig\Attribute\YieldReady;
/**
* Represents an empty node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
final class EmptyNode extends Node
{
public function __construct(int $lineno = 0)
{
parent::__construct([], [], $lineno);
}
public function setNode(string $name, Node $node): void
{
throw new \LogicException('EmptyNode cannot have children.');
}
}
@@ -25,4 +25,19 @@ abstract class AbstractExpression extends Node
{
return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator');
}
/**
* @return static
*/
public function setExplicitParentheses(): self
{
$this->setAttribute('with_parentheses', true);
return $this;
}
public function hasExplicitParentheses(): bool
{
return $this->hasAttribute('with_parentheses') && $this->getAttribute('with_parentheses');
}
}
@@ -12,9 +12,15 @@
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;
class ArrayExpression extends AbstractExpression
class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface, ReturnArrayInterface, CoercesChildrenToStringInterface
{
use SupportDefinedTestTrait;
private $index;
public function __construct(array $elements, int $lineno)
@@ -55,6 +61,31 @@ class ArrayExpression extends AbstractExpression
return false;
}
/**
* Checks if the array is a sequence (keys are sequential integers starting from 0).
*
* @internal
*/
public function isSequence(): bool
{
foreach ($this->getKeyValuePairs() as $i => $pair) {
$key = $pair['key'];
if ($key instanceof TempNameExpression) {
$keyValue = $key->getAttribute('name');
} elseif ($key instanceof ConstantExpression) {
$keyValue = $key->getAttribute('value');
} else {
return false;
}
if ($keyValue !== $i) {
return false;
}
}
return true;
}
public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
{
if (null === $key) {
@@ -64,72 +95,71 @@ class ArrayExpression extends AbstractExpression
array_push($this->nodes, $key, $value);
}
public function compile(Compiler $compiler): void
public function getStringCoercedChildNames(): array
{
$keyValuePairs = $this->getKeyValuePairs();
$needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
if ($needsArrayMergeSpread) {
$compiler->raw('CoreExtension::merge(');
}
$compiler->raw('[');
$first = true;
$reopenAfterMergeSpread = false;
$nextIndex = 0;
foreach ($keyValuePairs as $pair) {
if ($reopenAfterMergeSpread) {
$compiler->raw(', [');
$reopenAfterMergeSpread = false;
}
if ($needsArrayMergeSpread && $pair['value']->hasAttribute('spread')) {
$compiler->raw('], ')->subcompile($pair['value']);
$first = true;
$reopenAfterMergeSpread = true;
// 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;
}
if (!$first) {
$names[] = (string) ($i * 2);
}
return $names;
}
public function compile(Compiler $compiler): void
{
if ($this->definedTest) {
$compiler->repr(true);
return;
}
// Check for empty expressions which are only allowed in destructuring
foreach ($this->getKeyValuePairs() as $pair) {
if ($pair['value'] instanceof EmptyExpression) {
throw new SyntaxError('Empty array elements are only allowed in destructuring assignments.', $pair['value']->getTemplateLine(), $this->getSourceContext());
}
}
$compiler->raw('[');
$isSequence = true;
foreach ($this->getKeyValuePairs() as $i => $pair) {
if (0 !== $i) {
$compiler->raw(', ');
}
$first = false;
if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) {
$compiler->raw('...')->subcompile($pair['value']);
++$nextIndex;
$key = null;
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 {
$key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
if ($nextIndex !== $key) {
if (\is_int($key)) {
$nextIndex = $key + 1;
}
$compiler
->subcompile($pair['key'])
->raw(' => ')
;
} else {
++$nextIndex;
}
$compiler->subcompile($pair['value']);
// 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 (!$reopenAfterMergeSpread) {
$compiler->raw(']');
}
if ($needsArrayMergeSpread) {
$compiler->raw(')');
}
}
private function hasSpreadItem(array $pairs): bool
{
foreach ($pairs as $pair) {
if ($pair['value']->hasAttribute('spread')) {
return true;
if ($key !== $i) {
$isSequence = false;
}
}
return false;
if (!$isSequence && !$pair['value'] instanceof SpreadUnary) {
$compiler
->subcompile($pair['key'])
->raw(' => ')
;
}
$compiler->subcompile($pair['value']);
}
$compiler->raw(']');
}
}
@@ -12,6 +12,9 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
/**
@@ -23,6 +26,14 @@ class ArrowFunctionExpression extends AbstractExpression
{
public function __construct(AbstractExpression $expr, Node $names, $lineno)
{
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);
}
@@ -31,19 +42,7 @@ class ArrowFunctionExpression extends AbstractExpression
$compiler
->addDebugInfo($this)
->raw('function (')
;
foreach ($this->getNode('names') as $i => $name) {
if ($i) {
$compiler->raw(', ');
}
$compiler
->raw('$__')
->raw($name->getAttribute('name'))
->raw('__')
;
}
$compiler
->subcompile($this->getNode('names'))
->raw(') use ($context, $macros) { ')
;
foreach ($this->getNode('names') as $name) {
@@ -13,9 +13,26 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
class AssignNameExpression extends NameExpression
class AssignNameExpression extends ContextVariable
{
public function __construct(string $name, int $lineno)
{
if (self::class === static::class) {
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, AssignContextVariable::class);
}
// All names supported by ExpressionParser::parsePrimaryExpression() should be excluded
if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'], true)) {
throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno);
}
parent::__construct($name, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
@@ -16,10 +16,21 @@ use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
abstract class AbstractBinary extends AbstractExpression
abstract class AbstractBinary extends AbstractExpression implements BinaryInterface
{
/**
* @param AbstractExpression $left
* @param AbstractExpression $right
*/
public function __construct(Node $left, Node $right, int $lineno)
{
if (!$left instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $left::class);
}
if (!$right instanceof AbstractExpression) {
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);
}
parent::__construct(['left' => $left, 'right' => $right], [], $lineno);
}
@@ -13,8 +13,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class AddBinary extends AbstractBinary
class AddBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -13,9 +13,17 @@
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
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('&&');
@@ -0,0 +1,22 @@
<?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\Expression\Binary;
use Twig\Node\Expression\AbstractExpression;
/**
* @internal
*/
interface BinaryInterface
{
public function __construct(AbstractExpression $left, AbstractExpression $right, int $lineno);
}
@@ -13,8 +13,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class BitwiseAndBinary extends AbstractBinary
class BitwiseAndBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -13,8 +13,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class BitwiseOrBinary extends AbstractBinary
class BitwiseOrBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -13,8 +13,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class BitwiseXorBinary extends AbstractBinary
class BitwiseXorBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -13,11 +13,18 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnStringInterface;
class ConcatBinary extends AbstractBinary
class ConcatBinary extends AbstractBinary implements ReturnStringInterface, CoercesChildrenToStringInterface
{
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('.');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -13,8 +13,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class DivBinary extends AbstractBinary
class DivBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -0,0 +1,56 @@
<?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\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
{
/**
* @param AbstractExpression $left
* @param AbstractExpression $right
*/
public function __construct(Node $left, Node $right, int $lineno)
{
parent::__construct($left, $right, $lineno);
$this->setNode('test', TrueTest::wrap(clone $left));
$left->setAttribute('always_defined', true);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('((')
->subcompile($this->getNode('test'))
->raw(') ? (')
->subcompile($this->getNode('left'))
->raw(') : (')
->subcompile($this->getNode('right'))
->raw('))')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('?:');
}
public function getOperandNamesToEscape(): array
{
return ['left', 'right'];
}
}
@@ -12,8 +12,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnBoolInterface;
class EndsWithBinary extends AbstractBinary
class EndsWithBinary extends AbstractBinary implements ReturnBoolInterface
{
public function compile(Compiler $compiler): void
{
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class EqualBinary extends AbstractBinary
class EqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -36,4 +38,9 @@ class EqualBinary extends AbstractBinary
{
return $compiler->raw('==');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -12,8 +12,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class FloorDivBinary extends AbstractBinary
class FloorDivBinary extends AbstractBinary implements ReturnNumberInterface
{
public function compile(Compiler $compiler): void
{
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class GreaterBinary extends AbstractBinary
class GreaterBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -36,4 +38,9 @@ class GreaterBinary extends AbstractBinary
{
return $compiler->raw('>');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class GreaterEqualBinary extends AbstractBinary
class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -36,4 +38,9 @@ class GreaterEqualBinary extends AbstractBinary
{
return $compiler->raw('>=');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -12,8 +12,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnBoolInterface;
class HasEveryBinary extends AbstractBinary
class HasEveryBinary extends AbstractBinary implements ReturnBoolInterface
{
public function compile(Compiler $compiler): void
{
@@ -22,7 +23,7 @@ class HasEveryBinary extends AbstractBinary
->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)->getChecker()->isSandboxed($this->source))')
;
}
@@ -12,8 +12,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnBoolInterface;
class HasSomeBinary extends AbstractBinary
class HasSomeBinary extends AbstractBinary implements ReturnBoolInterface
{
public function compile(Compiler $compiler): void
{
@@ -22,7 +23,7 @@ class HasSomeBinary extends AbstractBinary
->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)->getChecker()->isSandboxed($this->source))')
;
}
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class InBinary extends AbstractBinary
class InBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -30,4 +32,9 @@ class InBinary extends AbstractBinary
{
return $compiler->raw('in');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class LessBinary extends AbstractBinary
class LessBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -36,4 +38,9 @@ class LessBinary extends AbstractBinary
{
return $compiler->raw('<');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class LessEqualBinary extends AbstractBinary
class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -36,4 +38,9 @@ class LessEqualBinary extends AbstractBinary
{
return $compiler->raw('<=');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -12,9 +12,37 @@
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
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));
try {
preg_match($regexp, '');
} finally {
restore_error_handler();
}
}
parent::__construct($left, $right, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
@@ -30,4 +58,9 @@ class MatchesBinary extends AbstractBinary
{
return $compiler->raw('');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -13,8 +13,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class ModBinary extends AbstractBinary
class ModBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -13,8 +13,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class MulBinary extends AbstractBinary
class MulBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class NotEqualBinary extends AbstractBinary
class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -36,4 +38,9 @@ class NotEqualBinary extends AbstractBinary
{
return $compiler->raw('!=');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class NotInBinary extends AbstractBinary
class NotInBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -30,4 +32,9 @@ class NotInBinary extends AbstractBinary
{
return $compiler->raw('not in');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
@@ -0,0 +1,23 @@
<?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\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnBoolInterface;
class NotSameAsBinary extends AbstractBinary implements ReturnBoolInterface
{
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('!==');
}
}
@@ -0,0 +1,71 @@
<?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\Expression\Binary;
use Twig\Compiler;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\OperatorEscapeInterface;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Expression\Test\NullTest;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Node;
use Twig\TwigTest;
final class NullCoalesceBinary extends AbstractBinary implements OperatorEscapeInterface
{
/**
* @param AbstractExpression $left
* @param AbstractExpression $right
*/
public function __construct(Node $left, Node $right, int $lineno)
{
parent::__construct($left, $right, $lineno);
$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', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
$left->getTemplateLine(),
);
}
$left->setAttribute('always_defined', true);
$this->setNode('test', $test);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('((')
->subcompile($this->getNode('test'))
->raw(') ? (')
->subcompile($this->getNode('left'))
->raw(') : (')
->subcompile($this->getNode('right'))
->raw('))')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('??');
}
public function getOperandNamesToEscape(): array
{
return ['left', 'right'];
}
}
@@ -0,0 +1,84 @@
<?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\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\AssignContextVariable;
use Twig\Node\Node;
/**
* @internal
*/
class ObjectDestructuringSetBinary extends AbstractBinary
{
/** @var list<array{property: string, variable: string}> */
private array $mappings = [];
/**
* @param ArrayExpression $left The array expression containing object/mapping destructuring properties
* @param AbstractExpression $right The expression providing values for assignment
*/
public function __construct(Node $left, Node $right, int $lineno)
{
if (!$left instanceof ArrayExpression) {
throw new \LogicException('Left side must be ArrayExpression for object/mapping destructuring.');
}
foreach ($left->getKeyValuePairs() as $pair) {
if (!$pair['value'] instanceof AssignContextVariable) {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno);
}
$this->mappings[] = [
'property' => $pair['key']->getAttribute('value'),
'variable' => $pair['value']->getAttribute('name'),
];
}
parent::__construct($left, $right, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$var = '$'.$compiler->getVarName();
$compiler->raw('[[');
foreach ($this->mappings as $i => $mapping) {
if ($i) {
$compiler->raw(', ');
}
$compiler->raw('$context[')->repr($mapping['variable'])->raw(']');
}
$compiler->raw('] = [');
foreach ($this->mappings as $i => $mapping) {
if ($i) {
$compiler->raw(', ');
}
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
if (0 === $i) {
$compiler->raw('('.$var.' = ')->subcompile($this->getNode('right'))->raw(')');
} else {
$compiler->raw($var);
}
$compiler->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('], '.$var.' = null][0]');
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('=');
}
}
@@ -13,9 +13,17 @@
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
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,8 +12,9 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnNumberInterface;
class PowerBinary extends AbstractBinary
class PowerBinary extends AbstractBinary implements ReturnNumberInterface
{
public function operator(Compiler $compiler): Compiler
{
@@ -12,8 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnArrayInterface;
class RangeBinary extends AbstractBinary
class RangeBinary extends AbstractBinary implements ReturnArrayInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -30,4 +32,9 @@ class RangeBinary extends AbstractBinary
{
return $compiler->raw('..');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
Loaded 100 of 1297 files, more files were not shown because too many files have changed in this diff. Show more