N°7854 - ⬆️ Bump twig version

This commit is contained in:
Eric Espie
2024-12-10 17:08:48 +01:00
parent 19559b08a7
commit 72ac4096c1
40 changed files with 2695 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
parameters:
ignoreErrors:
- # The method is dynamically generated by the CheckSecurityNode
message: '#^Call to an undefined method Twig\\Template\:\:checkSecurity\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Extension/CoreExtension.php
- # Avoid BC-break
message: '#^Constructor of class Twig\\Node\\ForNode has an unused parameter \$ifexpr\.$#'
identifier: constructor.unusedParameter
count: 1
path: src/Node/ForNode.php
- # 2 parameters will be required
message: '#^Method Twig\\Node\\IncludeNode\:\:addGetTemplate\(\) invoked with 2 parameters, 1 required\.$#'
identifier: arguments.count
count: 1
path: src/Node/IncludeNode.php
- # int|string will be supported in 4.x
message: '#^PHPDoc tag @param for parameter $name with type int|string is not subtype of native type string\.$#'
identifier: parameter.phpDocType
count: 5
path: src/Node/Node.php

View File

@@ -0,0 +1,9 @@
includes:
- phpstan-baseline.neon
parameters:
level: 3
paths:
- src
excludePaths:
- src/Test

View File

@@ -0,0 +1,184 @@
<?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>
*/
abstract class AbstractTwigCallable implements TwigCallableInterface
{
protected $options;
private $name;
private $dynamicName;
private $callable;
private $arguments;
public function __construct(string $name, $callable = null, array $options = [])
{
$this->name = $this->dynamicName = $name;
$this->callable = $callable;
$this->arguments = [];
$this->options = array_merge([
'needs_environment' => false,
'needs_context' => false,
'needs_charset' => false,
'is_variadic' => false,
'deprecation_info' => null,
'deprecated' => false,
'deprecating_package' => '',
'alternative' => null,
], $options);
if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) {
throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class));
}
if ($this->options['deprecated']) {
if ($this->options['deprecation_info']) {
throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.');
}
trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.');
$this->options['deprecation_info'] = new DeprecatedCallableInfo(
$this->options['deprecating_package'],
$this->options['deprecated'],
null,
$this->options['alternative'],
);
}
if ($this->options['deprecation_info']) {
$this->options['deprecation_info']->setName($name);
$this->options['deprecation_info']->setType($this->getType());
}
}
public function __toString(): string
{
return \sprintf('%s(%s)', static::class, $this->name);
}
public function getName(): string
{
return $this->name;
}
public function getDynamicName(): string
{
return $this->dynamicName;
}
public function getCallable()
{
return $this->callable;
}
public function getNodeClass(): string
{
return $this->options['node_class'];
}
public function needsCharset(): bool
{
return $this->options['needs_charset'];
}
public function needsEnvironment(): bool
{
return $this->options['needs_environment'];
}
public function needsContext(): bool
{
return $this->options['needs_context'];
}
/**
* @return static
*/
public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self
{
$new = clone $this;
$new->name = $name;
$new->dynamicName = $dynamicName;
$new->arguments = $arguments;
return $new;
}
/**
* @deprecated since Twig 3.12, use withDynamicArguments() instead
*/
public function setArguments(array $arguments): void
{
trigger_deprecation('twig/twig', '3.12', 'The "%s::setArguments()" method is deprecated, use "%s::withDynamicArguments()" instead.', static::class, static::class);
$this->arguments = $arguments;
}
public function getArguments(): array
{
return $this->arguments;
}
public function isVariadic(): bool
{
return $this->options['is_variadic'];
}
public function isDeprecated(): bool
{
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);
}
}

View File

@@ -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\Attribute;
/**
* Marks nodes that are ready to accept a TwigCallable instead of its name.
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class FirstClassTwigCallableReady
{
}

View File

@@ -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\Attribute;
/**
* Marks nodes that are ready for using "yield" instead of "echo" or "print()" for rendering.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class YieldReady
{
}

View File

@@ -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\Cache;
/**
* Chains several caches together.
*
* Cached items are fetched from the first cache having them in its data store.
* They are saved and deleted in all adapters at once.
*
* @author Quentin Devos <quentin@devos.pm>
*/
final class ChainCache implements CacheInterface, RemovableCacheInterface
{
/**
* @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
*/
public function __construct(
private iterable $caches,
) {
}
public function generateKey(string $name, string $className): string
{
return $className.'#'.$name;
}
public function write(string $key, string $content): void
{
$splitKey = $this->splitKey($key);
foreach ($this->caches as $cache) {
$cache->write($cache->generateKey(...$splitKey), $content);
}
}
public function load(string $key): void
{
[$name, $className] = $this->splitKey($key);
foreach ($this->caches as $cache) {
$cache->load($cache->generateKey($name, $className));
if (class_exists($className, false)) {
break;
}
}
}
public function getTimestamp(string $key): int
{
$splitKey = $this->splitKey($key);
foreach ($this->caches as $cache) {
if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) {
return $timestamp;
}
}
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[]
*/
private function splitKey(string $key): array
{
return array_reverse(explode('#', $key, 2));
}
}

View File

@@ -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\Cache;
/**
* Implements a cache on the filesystem that can only be read, not written to.
*
* @author Quentin Devos <quentin@devos.pm>
*/
class ReadOnlyFilesystemCache extends FilesystemCache
{
public function write(string $key, string $content): void
{
// Do nothing with the content, it's a read-only filesystem.
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Extension;
use Twig\NodeVisitor\YieldNotReadyNodeVisitor;
/**
* @internal to be removed in Twig 4
*/
final class YieldNotReadyExtension extends AbstractExtension
{
public function __construct(
private bool $useYield,
) {
}
public function getNodeVisitors(): array
{
return [new YieldNotReadyNodeVisitor($this->useYield)];
}
}

View File

@@ -0,0 +1,57 @@
<?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;
use Twig\Compiler;
/**
* Represents a node for which we need to capture the output.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CaptureNode extends Node
{
public function __construct(Node $body, int $lineno)
{
parent::__construct(['body' => $body], ['raw' => false], $lineno);
}
public function compile(Compiler $compiler): void
{
$useYield = $compiler->getEnvironment()->useYield();
if (!$this->getAttribute('raw')) {
$compiler->raw("('' === \$tmp = ");
}
$compiler
->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput(')
->raw("(function () use (&\$context, \$macros, \$blocks) {\n")
->indent()
->subcompile($this->getNode('body'))
->write("yield from [];\n")
->outdent()
->write('})()')
;
if ($useYield) {
$compiler->raw(', false))');
} else {
$compiler->raw(')');
}
if (!$this->getAttribute('raw')) {
$compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset());");
} else {
$compiler->raw(';');
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\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.');
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* 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;
class XorBinary extends AbstractBinary
{
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('xor');
}
}

View File

@@ -0,0 +1,45 @@
<?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\Filter;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Node;
use Twig\TwigFilter;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RawFilter extends FilterExpression
{
/**
* @param AbstractExpression $node
*/
#[FirstClassTwigCallableReady]
public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0)
{
if (!$node instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
}
parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine());
}
public function compile(Compiler $compiler): void
{
$compiler->subcompile($this->getNode('node'));
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Twig\Node\Expression\FunctionNode;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
class EnumCasesFunction extends FunctionExpression
{
public function compile(Compiler $compiler): void
{
$arguments = $this->getNode('arguments');
if ($arguments->hasNode('enum')) {
$firstArgument = $arguments->getNode('enum');
} elseif ($arguments->hasNode('0')) {
$firstArgument = $arguments->getNode('0');
} else {
$firstArgument = null;
}
if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) {
parent::compile($compiler);
return;
}
$value = $firstArgument->getAttribute('value');
if (!\is_string($value)) {
throw new SyntaxError('The first argument of the "enum_cases" function must be a string.', $this->getTemplateLine(), $this->getSourceContext());
}
if (!enum_exists($value)) {
throw new SyntaxError(\sprintf('The first argument of the "enum_cases" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext());
}
$compiler->raw(\sprintf('%s::cases()', $value));
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Twig\Node\Expression\FunctionNode;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
class EnumFunction extends FunctionExpression
{
public function compile(Compiler $compiler): void
{
$arguments = $this->getNode('arguments');
if ($arguments->hasNode('enum')) {
$firstArgument = $arguments->getNode('enum');
} elseif ($arguments->hasNode('0')) {
$firstArgument = $arguments->getNode('0');
} else {
$firstArgument = null;
}
if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) {
parent::compile($compiler);
return;
}
$value = $firstArgument->getAttribute('value');
if (!\is_string($value)) {
throw new SyntaxError('The first argument of the "enum" function must be a string.', $this->getTemplateLine(), $this->getSourceContext());
}
if (!enum_exists($value)) {
throw new SyntaxError(\sprintf('The first argument of the "enum" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext());
}
if (!$cases = $value::cases()) {
throw new SyntaxError(\sprintf('The first argument of the "enum" function must be a non-empty enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext());
}
$compiler->raw(\sprintf('%s::%s', $value, $cases[0]->name));
}
}

View File

@@ -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;
use Twig\Compiler;
use Twig\Node\Expression\Variable\TemplateVariable;
/**
* Represents a macro call node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class MacroReferenceExpression extends AbstractExpression
{
public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno)
{
parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
}
public function compile(Compiler $compiler): void
{
if ($this->getAttribute('is_defined_test')) {
$compiler
->subcompile($this->getNode('template'))
->raw('->hasMacro(')
->repr($this->getAttribute('name'))
->raw(', $context')
->raw(')')
;
return;
}
$compiler
->subcompile($this->getNode('template'))
->raw('->getTemplateForMacro(')
->repr($this->getAttribute('name'))
->raw(', $context, ')
->repr($this->getTemplateLine())
->raw(', $this->getSourceContext())')
->raw(\sprintf('->%s', $this->getAttribute('name')))
->raw('(...')
->subcompile($this->getNode('arguments'))
->raw(')')
;
}
}

View File

@@ -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\Unary;
use Twig\Compiler;
final class SpreadUnary extends AbstractUnary
{
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('...');
}
}

View File

@@ -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\Unary;
use Twig\Compiler;
final class StringCastUnary extends AbstractUnary
{
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('(string)');
}
}

View File

@@ -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\Node\Expression\Variable;
use Twig\Node\Expression\AssignNameExpression;
final class AssignContextVariable extends AssignNameExpression
{
}

View File

@@ -0,0 +1,44 @@
<?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\Variable;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
final class AssignTemplateVariable extends AbstractExpression
{
public function __construct(TemplateVariable $var, bool $global = true)
{
parent::__construct(['var' => $var], ['global' => $global], $var->getTemplateLine());
}
public function compile(Compiler $compiler): void
{
/** @var TemplateVariable $var */
$var = $this->nodes['var'];
$compiler
->addDebugInfo($this)
->write('$macros[')
->string($var->getName($compiler))
->raw('] = ')
;
if ($this->getAttribute('global')) {
$compiler
->raw('$this->macros[')
->string($var->getName($compiler))
->raw('] = ')
;
}
}
}

View File

@@ -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\Node\Expression\Variable;
use Twig\Node\Expression\NameExpression;
final class ContextVariable extends NameExpression
{
}

View File

@@ -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\Node\Expression\Variable;
use Twig\Node\Expression\TempNameExpression;
final class LocalVariable extends TempNameExpression
{
}

View File

@@ -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\Node\Expression\Variable;
use Twig\Compiler;
use Twig\Node\Expression\TempNameExpression;
class TemplateVariable extends TempNameExpression
{
public function getName(Compiler $compiler): string
{
if (null === $this->getAttribute('name')) {
$this->setAttribute('name', $compiler->getVarName());
}
return $this->getAttribute('name');
}
public function compile(Compiler $compiler): void
{
$name = $this->getName($compiler);
if ('_self' === $name) {
$compiler->raw('$this');
} else {
$compiler
->raw('$macros[')
->string($name)
->raw(']')
;
}
}
}

View File

@@ -0,0 +1,46 @@
<?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;
/**
* Represents a deprecation for a named node or attribute on a Node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class NameDeprecation
{
private $package;
private $version;
private $newName;
public function __construct(string $package = '', string $version = '', string $newName = '')
{
$this->package = $package;
$this->version = $version;
$this->newName = $newName;
}
public function getPackage(): string
{
return $this->package;
}
public function getVersion(): string
{
return $this->version;
}
public function getNewName(): string
{
return $this->newName;
}
}

View File

@@ -0,0 +1,28 @@
<?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 list of nodes.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
final class Nodes extends Node
{
public function __construct(array $nodes = [], int $lineno = 0)
{
parent::__construct($nodes, [], $lineno);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* Represents a types node.
*
* @author Jeroen Versteeg <jeroen@alisqi.com>
*/
#[YieldReady]
class TypesNode extends Node
{
/**
* @param array<string, array{type: string, optional: bool}> $types
*/
public function __construct(array $types, int $lineno)
{
parent::__construct([], ['mapping' => $types], $lineno);
}
public function compile(Compiler $compiler)
{
// Don't compile anything.
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\NodeVisitor;
use Twig\Attribute\YieldReady;
use Twig\Environment;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
/**
* @internal to be removed in Twig 4
*/
final class YieldNotReadyNodeVisitor implements NodeVisitorInterface
{
private $yieldReadyNodes = [];
public function __construct(
private bool $useYield,
) {
}
public function enterNode(Node $node, Environment $env): Node
{
$class = \get_class($node);
if ($node instanceof AbstractExpression || isset($this->yieldReadyNodes[$class])) {
return $node;
}
if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) {
if ($this->useYield) {
throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class));
}
trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class);
}
return $node;
}
public function leaveNode(Node $node, Environment $env): ?Node
{
return $node;
}
public function getPriority(): int
{
return 255;
}
}

View File

@@ -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;
/**
* Represents a precedence change for an operator.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class OperatorPrecedenceChange
{
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;
}
}

View File

@@ -0,0 +1,541 @@
<?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.
*/
use Twig\Environment;
use Twig\Extension\CoreExtension;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_cycle($values, $position)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::cycle($values, $position);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_random(Environment $env, $values = null, $max = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::random($env->getCharset(), $values, $max);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_date_modify_filter(Environment $env, $date, $modifier)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_sprintf($format, ...$values)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::sprintf($format, ...$values);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_date_converter(Environment $env, $date = null, $timezone = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_replace_filter($str, $from)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::replace($str, $from);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_round($value, $precision = 0, $method = 'common')
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::round($value, $precision, $method);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_urlencode_filter($url)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::urlencode($url);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_merge(...$arrays)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::merge(...$arrays);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::slice($env->getCharset(), $item, $start, $length, $preserveKeys);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_first(Environment $env, $item)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::first($env->getCharset(), $item);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_last(Environment $env, $item)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::last($env->getCharset(), $item);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_join_filter($value, $glue = '', $and = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::join($value, $glue, $and);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::split($env->getCharset(), $value, $delimiter, $limit);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_get_array_keys_filter($array)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::keys($array);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::reverse($env->getCharset(), $item, $preserveKeys);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_sort_filter(Environment $env, $array, $arrow = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::sort($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_matches(string $regexp, ?string $str)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::matches($regexp, $str);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_trim_filter($string, $characterMask = null, $side = 'both')
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::trim($string, $characterMask, $side);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_nl2br($string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::nl2br($string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_spaceless($content)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::spaceless($content);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_convert_encoding($string, $to, $from)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::convertEncoding($string, $to, $from);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_length_filter(Environment $env, $thing)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::length($env->getCharset(), $thing);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_upper_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::upper($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_lower_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::lower($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_striptags($string, $allowable_tags = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::striptags($string, $allowable_tags);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_title_string_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::titleCase($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_capitalize_string_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::capitalize($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_test_empty($value)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::testEmpty($value);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_test_iterable($value)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return is_iterable($value);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_source(Environment $env, $name, $ignoreMissing = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::source($env, $name, $ignoreMissing);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_constant($constant, $object = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::constant($constant, $object);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_constant_is_defined($constant, $object = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::constant($constant, $object, true);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::batch($items, $size, $fill, $preserveKeys);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_column($array, $name, $index = null): array
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::column($array, $name, $index);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_filter(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::filter($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_map(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::map($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::reduce($env, $array, $arrow, $initial);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_some(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::arraySome($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_every(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::arrayEvery($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
CoreExtension::checkArrow($env, $arrow, $thing, $type);
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Extension\DebugExtension;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_var_dump(Environment $env, $context, ...$vars)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
DebugExtension::dump($env, $context, ...$vars);
}

View File

@@ -0,0 +1,51 @@
<?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.
*/
use Twig\Environment;
use Twig\Extension\EscaperExtension;
use Twig\Node\Node;
use Twig\Runtime\EscaperRuntime;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_raw_filter($string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $string;
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getRuntime(EscaperRuntime::class)->escape($string, $strategy, $charset, $autoescape);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_escape_filter_is_safe(Node $filterArgs)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return EscaperExtension::escapeFilterIsSafe($filterArgs);
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Extension\StringLoaderExtension;
use Twig\TemplateWrapper;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return StringLoaderExtension::templateFromString($env, $template, $name);
}

View File

@@ -0,0 +1,334 @@
<?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\Runtime;
use Twig\Error\RuntimeError;
use Twig\Extension\RuntimeExtensionInterface;
use Twig\Markup;
final class EscaperRuntime implements RuntimeExtensionInterface
{
/** @var array<string, callable(string $string, string $charset): string> */
private $escapers = [];
/** @internal */
public $safeClasses = [];
/** @internal */
public $safeLookup = [];
public function __construct(
private $charset = 'UTF-8',
) {
}
/**
* Defines a new escaper to be used via the escape filter.
*
* @param string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable(string $string, string $charset): string $callable A valid PHP callable
*/
public function setEscaper($strategy, callable $callable)
{
$this->escapers[$strategy] = $callable;
}
/**
* Gets all defined escapers.
*
* @return array<string, callable(string $string, string $charset): string> An array of escapers
*/
public function getEscapers()
{
return $this->escapers;
}
/**
* @param array<class-string<\Stringable>, string[]> $safeClasses
*/
public function setSafeClasses(array $safeClasses = [])
{
$this->safeClasses = [];
$this->safeLookup = [];
foreach ($safeClasses as $class => $strategies) {
$this->addSafeClass($class, $strategies);
}
}
/**
* @param class-string<\Stringable> $class
* @param string[] $strategies
*/
public function addSafeClass(string $class, array $strategies)
{
$class = ltrim($class, '\\');
if (!isset($this->safeClasses[$class])) {
$this->safeClasses[$class] = [];
}
$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
foreach ($strategies as $strategy) {
$this->safeLookup[$strategy][$class] = true;
}
}
/**
* Escapes a string.
*
* @param mixed $string The value to be escaped
* @param string $strategy The escaping strategy
* @param string|null $charset The charset
* @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
*
* @throws RuntimeError
*/
public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false)
{
if ($autoescape && $string instanceof Markup) {
return $string;
}
if (!\is_string($string)) {
if ($string instanceof \Stringable) {
if ($autoescape) {
$c = \get_class($string);
if (!isset($this->safeClasses[$c])) {
$this->safeClasses[$c] = [];
foreach (class_parents($string) + class_implements($string) as $class) {
if (isset($this->safeClasses[$class])) {
$this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class]));
foreach ($this->safeClasses[$class] as $s) {
$this->safeLookup[$s][$c] = true;
}
}
}
}
if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) {
return (string) $string;
}
}
$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
// we return the input as is (which can be of any type)
return $string;
}
}
if ('' === $string) {
return '';
}
$charset = $charset ?: $this->charset;
switch ($strategy) {
case 'html':
// see https://www.php.net/htmlspecialchars
// Using a static variable to avoid initializing the array
// each time the function is called. Moving the declaration on the
// top of the function slow downs other escaping strategies.
static $htmlspecialcharsCharsets = [
'ISO-8859-1' => true, 'ISO8859-1' => true,
'ISO-8859-15' => true, 'ISO8859-15' => true,
'utf-8' => true, 'UTF-8' => true,
'CP866' => true, 'IBM866' => true, '866' => true,
'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
'1251' => true,
'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
'BIG5' => true, '950' => true,
'GB2312' => true, '936' => true,
'BIG5-HKSCS' => true,
'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
'EUC-JP' => true, 'EUCJP' => true,
'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
];
if (isset($htmlspecialcharsCharsets[$charset])) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
// cache the lowercase variant for future iterations
$htmlspecialcharsCharsets[$charset] = true;
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
$string = $this->convertEncoding($string, 'UTF-8', $charset);
$string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
return iconv('UTF-8', $charset, $string);
case 'js':
// escape all non-alphanumeric characters
// into their \x or \uHHHH representations
if ('UTF-8' !== $charset) {
$string = $this->convertEncoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
$char = $matches[0];
/*
* A few characters have short escape sequences in JSON and JavaScript.
* Escape sequences supported only by JavaScript, not JSON, are omitted.
* \" is also supported but omitted, because the resulting string is not HTML safe.
*/
static $shortMap = [
'\\' => '\\\\',
'/' => '\\/',
"\x08" => '\b',
"\x0C" => '\f',
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
];
if (isset($shortMap[$char])) {
return $shortMap[$char];
}
$codepoint = mb_ord($char, 'UTF-8');
if (0x10000 > $codepoint) {
return \sprintf('\u%04X', $codepoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
$u = $codepoint - 0x10000;
$high = 0xD800 | ($u >> 10);
$low = 0xDC00 | ($u & 0x3FF);
return \sprintf('\u%04X\u%04X', $high, $low);
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'css':
if ('UTF-8' !== $charset) {
$string = $this->convertEncoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
$char = $matches[0];
return \sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'html_attr':
if ('UTF-8' !== $charset) {
$string = $this->convertEncoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
/**
* This function is adapted from code coming from Zend Framework.
*
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://framework.zend.com/license/new-bsd New BSD License
*/
$chr = $matches[0];
$ord = \ord($chr);
/*
* The following replaces characters undefined in HTML with the
* hex entity for the Unicode replacement character.
*/
if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) {
return '&#xFFFD;';
}
/*
* Check if the current character to escape has a name entity we should
* replace it with while grabbing the hex value of the character.
*/
if (1 === \strlen($chr)) {
/*
* While HTML supports far more named entities, the lowest common denominator
* has become HTML5's XML Serialisation which is restricted to the those named
* entities that XML supports. Using HTML entities would result in this error:
* XML Parsing Error: undefined entity
*/
static $entityMap = [
34 => '&quot;', /* quotation mark */
38 => '&amp;', /* ampersand */
60 => '&lt;', /* less-than sign */
62 => '&gt;', /* greater-than sign */
];
if (isset($entityMap[$ord])) {
return $entityMap[$ord];
}
return \sprintf('&#x%02X;', $ord);
}
/*
* Per OWASP recommendations, we'll use hex entities for any other
* characters where a named entity does not exist.
*/
return \sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'url':
return rawurlencode($string);
default:
if (\array_key_exists($strategy, $this->escapers)) {
return $this->escapers[$strategy]($string, $charset);
}
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers)));
throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies));
}
}
private function convertEncoding(string $string, string $to, string $from)
{
if (!\function_exists('iconv')) {
throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
}
return iconv($from, $to, $string);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
use Twig\Source;
/**
* Interface for a class that can optionally enable the sandbox mode based on a template's Twig\Source.
*
* @author Yaakov Saxon
*/
interface SourcePolicyInterface
{
public function enableSandbox(Source $source): bool;
}

View File

@@ -0,0 +1,69 @@
<?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\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\EmptyNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Token;
/**
* @internal
*/
final class GuardTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$typeToken = $stream->expect(Token::NAME_TYPE);
if (!in_array($typeToken->getValue(), ['function', 'filter', 'test'])) {
throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext());
}
$method = 'get'.$typeToken->getValue();
$nameToken = $stream->expect(Token::NAME_TYPE);
$exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue());
$stream->expect(Token::BLOCK_END_TYPE);
if ($exists) {
$body = $this->parser->subparse([$this, 'decideGuardFork']);
} else {
$body = new EmptyNode();
$this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardFork']);
}
$else = new EmptyNode();
if ('else' === $stream->next()->getValue()) {
$stream->expect(Token::BLOCK_END_TYPE);
$else = $this->parser->subparse([$this, 'decideGuardEnd'], true);
}
$stream->expect(Token::BLOCK_END_TYPE);
return new Nodes([$exists ? $body : $else]);
}
public function decideGuardFork(Token $token): bool
{
return $token->test(['else', 'endguard']);
}
public function decideGuardEnd(Token $token): bool
{
return $token->test(['endguard']);
}
public function getTag(): string
{
return 'guard';
}
}

View File

@@ -0,0 +1,86 @@
<?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\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\Node;
use Twig\Node\TypesNode;
use Twig\Token;
use Twig\TokenStream;
/**
* Declare variable types.
*
* {% types {foo: 'number', bar?: 'string'} %}
*
* @author Jeroen Versteeg <jeroen@alisqi.com>
*
* @internal
*/
final class TypesTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$types = $this->parseSimpleMappingExpression($stream);
$stream->expect(Token::BLOCK_END_TYPE);
return new TypesNode($types, $token->getLine());
}
/**
* @return array<string, array{type: string, optional: bool}>
*
* @throws SyntaxError
*/
private function parseSimpleMappingExpression(TokenStream $stream): array
{
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
$types = [];
$first = true;
while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma');
// trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
break;
}
}
$first = false;
$nameToken = $stream->expect(Token::NAME_TYPE);
$isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?');
$stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)');
$valueToken = $stream->expect(Token::STRING_TYPE);
$types[$nameToken->getValue()] = [
'type' => $valueToken->getValue(),
'optional' => $isOptional,
];
}
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
return $types;
}
public function getTag(): string
{
return 'types';
}
}

View File

@@ -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;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TwigCallableInterface extends \Stringable
{
public function getName(): string;
public function getType(): string;
public function getDynamicName(): string;
/**
* @return callable|array{class-string, string}|null
*/
public function getCallable();
public function getNodeClass(): string;
public function needsCharset(): bool;
public function needsEnvironment(): bool;
public function needsContext(): bool;
public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self;
public function getArguments(): array;
public function isVariadic(): bool;
public function isDeprecated(): bool;
public function getDeprecatingPackage(): string;
public function getDeprecatedVersion(): string;
public function getAlternative(): ?string;
public function getMinimalNumberOfRequiredArguments(): int;
}

View File

@@ -0,0 +1,219 @@
<?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\Util;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\VariadicExpression;
use Twig\Node\Node;
use Twig\TwigCallableInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class CallableArgumentsExtractor
{
private ReflectionCallable $rc;
public function __construct(
private Node $node,
private TwigCallableInterface $twigCallable,
) {
$this->rc = new ReflectionCallable($twigCallable);
}
/**
* @return array<Node>
*/
public function extractArguments(Node $arguments): array
{
$extractedArguments = [];
$extractedArgumentNameMap = [];
$named = false;
foreach ($arguments as $name => $node) {
if (!\is_int($name)) {
$named = true;
} elseif ($named) {
throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
$extractedArguments[$normalizedName = $this->normalizeName($name)] = $node;
$extractedArgumentNameMap[$normalizedName] = $name;
}
if (!$named && !$this->twigCallable->isVariadic()) {
$min = $this->twigCallable->getMinimalNumberOfRequiredArguments();
if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) {
$argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName());
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
return $extractedArguments;
}
if (!$callable = $this->twigCallable->getCallable()) {
if ($named) {
throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()));
}
throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()));
}
[$callableParameters, $isPhpVariadic] = $this->getCallableParameters();
$arguments = [];
$callableParameterNames = [];
$missingArguments = [];
$optionalArguments = [];
$pos = 0;
foreach ($callableParameters as $callableParameter) {
$callableParameterName = $callableParameter->name;
if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) {
if ('start' === $callableParameterName) {
$callableParameterName = 'low';
} elseif ('end' === $callableParameterName) {
$callableParameterName = 'high';
}
}
$callableParameterNames[] = $callableParameterName;
$normalizedCallableParameterName = $this->normalizeName($callableParameterName);
if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) {
if (\array_key_exists($pos, $extractedArguments)) {
throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
if (\count($missingArguments)) {
throw new SyntaxError(\sprintf(
'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
$callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
$arguments = array_merge($arguments, $optionalArguments);
$arguments[] = $extractedArguments[$normalizedCallableParameterName];
unset($extractedArguments[$normalizedCallableParameterName]);
$optionalArguments = [];
} elseif (\array_key_exists($pos, $extractedArguments)) {
$arguments = array_merge($arguments, $optionalArguments);
$arguments[] = $extractedArguments[$pos];
unset($extractedArguments[$pos]);
$optionalArguments = [];
++$pos;
} elseif ($callableParameter->isDefaultValueAvailable()) {
$optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), $this->node->getTemplateLine());
} elseif ($callableParameter->isOptional()) {
if (!$extractedArguments) {
break;
}
$missingArguments[] = $callableParameterName;
} else {
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
}
if ($this->twigCallable->isVariadic()) {
$arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], $this->node->getTemplateLine()) : new ArrayExpression([], $this->node->getTemplateLine());
foreach ($extractedArguments as $key => $value) {
if (\is_int($key)) {
$arbitraryArguments->addElement($value);
} else {
$originalKey = $extractedArgumentNameMap[$key];
if ($originalKey !== $this->toSnakeCase($originalKey)) {
trigger_deprecation('twig/twig', '3.15', \sprintf('Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "%s" to "%s" in "%s" at line %d.', $originalKey, $this->toSnakeCase($originalKey), $this->node->getSourceContext()->getName(), $this->node->getTemplateLine()));
}
$arbitraryArguments->addElement($value, new ConstantExpression($this->toSnakeCase($originalKey), $this->node->getTemplateLine()));
// I Twig 4.0, don't convert the key:
// $arbitraryArguments->addElement($value, new ConstantExpression($originalKey, $this->node->getTemplateLine()));
}
unset($extractedArguments[$key]);
}
if ($arbitraryArguments->count()) {
$arguments = array_merge($arguments, $optionalArguments);
$arguments[] = $arbitraryArguments;
}
}
if ($extractedArguments) {
$unknownArgument = null;
foreach ($extractedArguments as $extractedArgument) {
if ($extractedArgument instanceof Node) {
$unknownArgument = $extractedArgument;
break;
}
}
throw new SyntaxError(
\sprintf(
'Unknown argument%s "%s" for %s "%s(%s)".',
\count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames))
),
$unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(),
$unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext()
);
}
return $arguments;
}
private function normalizeName(string $name): string
{
return strtolower(str_replace('_', '', $name));
}
private function toSnakeCase(string $name): string
{
return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z0-9])([A-Z])/'], '\1_\2', $name));
}
private function getCallableParameters(): array
{
$parameters = $this->rc->getReflector()->getParameters();
if ($this->node->hasNode('node')) {
array_shift($parameters);
}
if ($this->twigCallable->needsCharset()) {
array_shift($parameters);
}
if ($this->twigCallable->needsEnvironment()) {
array_shift($parameters);
}
if ($this->twigCallable->needsContext()) {
array_shift($parameters);
}
foreach ($this->twigCallable->getArguments() as $argument) {
array_shift($parameters);
}
$isPhpVariadic = false;
if ($this->twigCallable->isVariadic()) {
$argument = end($parameters);
$isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName();
if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) {
array_pop($parameters);
} elseif ($argument && $argument->isVariadic()) {
array_pop($parameters);
$isPhpVariadic = true;
} else {
throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()));
}
}
return [$parameters, $isPhpVariadic];
}
}

View File

@@ -0,0 +1,92 @@
<?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\Util;
use Twig\TwigCallableInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class ReflectionCallable
{
private $reflector;
private $callable;
private $name;
public function __construct(
TwigCallableInterface $twigCallable,
) {
$callable = $twigCallable->getCallable();
if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
}
if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
$this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]);
$this->callable = $callable;
$this->name = $r->class.'::'.$r->name;
return;
}
$checkVisibility = $callable instanceof \Closure;
try {
$closure = \Closure::fromCallable($callable);
} catch (\TypeError $e) {
throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e);
}
$this->reflector = $r = new \ReflectionFunction($closure);
if (str_contains($r->name, '{closure')) {
$this->callable = $callable;
$this->name = 'Closure';
return;
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$this->name = get_debug_type($object).'::'.$r->name;
} elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
$callable = [$class->name, $r->name];
$this->name = $class->name.'::'.$r->name;
} elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
$callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name];
$this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
} else {
$callable = $this->name = $r->name;
}
if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
$callable = $r->getClosure();
}
$this->callable = $callable;
}
public function getReflector(): \ReflectionFunctionAbstract
{
return $this->reflector;
}
public function getCallable()
{
return $this->callable;
}
public function getName(): string
{
return $this->name;
}
}