Update Twig to v2.15.4

This commit is contained in:
slawkens
2023-02-02 10:37:45 +01:00
parent e552bcfe82
commit 130f7ba405
309 changed files with 3802 additions and 4005 deletions

View File

@@ -11,17 +11,8 @@
namespace Twig\Extension;
use Twig\Environment;
abstract class AbstractExtension implements ExtensionInterface
{
/**
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_InitRuntimeInterface instead
*/
public function initRuntime(Environment $environment)
{
}
public function getTokenParsers()
{
return [];
@@ -51,22 +42,6 @@ abstract class AbstractExtension implements ExtensionInterface
{
return [];
}
/**
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_GlobalsInterface instead
*/
public function getGlobals()
{
return [];
}
/**
* @deprecated since 1.26 (to be removed in 2.0), not used anymore internally
*/
public function getName()
{
return \get_class($this);
}
}
class_alias('Twig\Extension\AbstractExtension', 'Twig_Extension');

File diff suppressed because it is too large Load Diff

View File

@@ -12,10 +12,7 @@
namespace Twig\Extension {
use Twig\TwigFunction;
/**
* @final
*/
class DebugExtension extends AbstractExtension
final class DebugExtension extends AbstractExtension
{
public function getFunctions()
{
@@ -33,11 +30,6 @@ class DebugExtension extends AbstractExtension
new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
];
}
public function getName()
{
return 'debug';
}
}
class_alias('Twig\Extension\DebugExtension', 'Twig_Extension_Debug');
@@ -48,7 +40,7 @@ use Twig\Environment;
use Twig\Template;
use Twig\TemplateWrapper;
function twig_var_dump(Environment $env, $context, array $vars = [])
function twig_var_dump(Environment $env, $context, ...$vars)
{
if (!$env->isDebug()) {
return;
@@ -66,9 +58,7 @@ function twig_var_dump(Environment $env, $context, array $vars = [])
var_dump($vars);
} else {
foreach ($vars as $var) {
var_dump($var);
}
var_dump(...$vars);
}
return ob_get_clean();

View File

@@ -10,16 +10,21 @@
*/
namespace Twig\Extension {
use Twig\FileExtensionEscapingStrategy;
use Twig\NodeVisitor\EscaperNodeVisitor;
use Twig\TokenParser\AutoEscapeTokenParser;
use Twig\TwigFilter;
/**
* @final
*/
class EscaperExtension extends AbstractExtension
final class EscaperExtension extends AbstractExtension
{
protected $defaultStrategy;
private $defaultStrategy;
private $escapers = [];
/** @internal */
public $safeClasses = [];
/** @internal */
public $safeLookup = [];
/**
* @param string|false|callable $defaultStrategy An escaping strategy
@@ -44,6 +49,8 @@ class EscaperExtension extends AbstractExtension
public function getFilters()
{
return [
new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
];
}
@@ -58,21 +65,8 @@ class EscaperExtension extends AbstractExtension
*/
public function setDefaultStrategy($defaultStrategy)
{
// for BC
if (true === $defaultStrategy) {
@trigger_error('Using "true" as the default strategy is deprecated since version 1.21. Use "html" instead.', E_USER_DEPRECATED);
$defaultStrategy = 'html';
}
if ('filename' === $defaultStrategy) {
@trigger_error('Using "filename" as the default strategy is deprecated since version 1.27. Use "name" instead.', E_USER_DEPRECATED);
$defaultStrategy = 'name';
}
if ('name' === $defaultStrategy) {
$defaultStrategy = ['\Twig\FileExtensionEscapingStrategy', 'guess'];
$defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess'];
}
$this->defaultStrategy = $defaultStrategy;
@@ -96,9 +90,47 @@ class EscaperExtension extends AbstractExtension
return $this->defaultStrategy;
}
public function getName()
/**
* 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 $callable A valid PHP callable
*/
public function setEscaper($strategy, callable $callable)
{
return 'escaper';
$this->escapers[$strategy] = $callable;
}
/**
* Gets all defined escapers.
*
* @return callable[] An array of escapers
*/
public function getEscapers()
{
return $this->escapers;
}
public function setSafeClasses(array $safeClasses = [])
{
$this->safeClasses = [];
$this->safeLookup = [];
foreach ($safeClasses as $class => $strategies) {
$this->addSafeClass($class, $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;
}
}
}
@@ -106,6 +138,14 @@ class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
}
namespace {
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
use Twig\Markup;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
/**
* Marks a variable as being safe.
*
@@ -117,4 +157,272 @@ function twig_raw_filter($string)
{
return $string;
}
/**
* Escapes a string.
*
* @param mixed $string The value to be escaped
* @param string $strategy The escaping strategy
* @param string $charset The charset
* @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
*
* @return string
*/
function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
{
if ($autoescape && $string instanceof Markup) {
return $string;
}
if (!\is_string($string)) {
if (\is_object($string) && method_exists($string, '__toString')) {
if ($autoescape) {
$c = \get_class($string);
$ext = $env->getExtension(EscaperExtension::class);
if (!isset($ext->safeClasses[$c])) {
$ext->safeClasses[$c] = [];
foreach (class_parents($string) + class_implements($string) as $class) {
if (isset($ext->safeClasses[$class])) {
$ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
foreach ($ext->safeClasses[$class] as $s) {
$ext->safeLookup[$s][$c] = true;
}
}
}
}
if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
return (string) $string;
}
}
$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
return $string;
}
}
if ('' === $string) {
return '';
}
if (null === $charset) {
$charset = $env->getCharset();
}
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 = twig_convert_encoding($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 = twig_convert_encoding($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 = twig_convert_encoding($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 = twig_convert_encoding($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:
// check the ones set on CoreExtension for BC (to be removed in 3.0)
$legacyEscapers = $env->getExtension(CoreExtension::class)->getEscapers(false);
if (array_key_exists($strategy, $legacyEscapers)) {
return $legacyEscapers[$strategy]($env, $string, $charset);
}
$escapers = $env->getExtension(EscaperExtension::class)->getEscapers();
if (array_key_exists($strategy, $escapers)) {
return $escapers[$strategy]($env, $string, $charset);
}
$escapers = array_merge($legacyEscapers, $escapers);
$validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
}
}
/**
* @internal
*/
function twig_escape_filter_is_safe(Node $filterArgs)
{
foreach ($filterArgs as $arg) {
if ($arg instanceof ConstantExpression) {
return [$arg->getAttribute('value')];
}
return [];
}
return ['html'];
}
}

View File

@@ -11,7 +11,6 @@
namespace Twig\Extension;
use Twig\Environment;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
@@ -25,15 +24,6 @@ use Twig\TwigTest;
*/
interface ExtensionInterface
{
/**
* Initializes the runtime environment.
*
* This is where you can load some file that contains filter functions for instance.
*
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_InitRuntimeInterface instead
*/
public function initRuntime(Environment $environment);
/**
* Returns the token parser instances to add to the existing list.
*
@@ -75,24 +65,6 @@ interface ExtensionInterface
* @return array<array> First array of unary operators, second array of binary operators
*/
public function getOperators();
/**
* Returns a list of global variables to add to the existing list.
*
* @return array An array of global variables
*
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_GlobalsInterface instead
*/
public function getGlobals();
/**
* Returns the name of the extension.
*
* @return string The extension name
*
* @deprecated since 1.26 (to be removed in 2.0), not used anymore internally
*/
public function getName();
}
class_alias('Twig\Extension\ExtensionInterface', 'Twig_ExtensionInterface');

View File

@@ -21,6 +21,12 @@ namespace Twig\Extension;
*/
interface GlobalsInterface
{
/**
* Returns a list of global variables to add to the existing list.
*
* @return array An array of global variables
*/
public function getGlobals();
}
class_alias('Twig\Extension\GlobalsInterface', 'Twig_Extension_GlobalsInterface');

View File

@@ -11,6 +11,8 @@
namespace Twig\Extension;
use Twig\Environment;
/**
* Enables usage of the deprecated Twig\Extension\AbstractExtension::initRuntime() method.
*
@@ -18,9 +20,17 @@ namespace Twig\Extension;
* deprecated initRuntime() method in your extensions.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Twig 2.7, to be removed in 3.0
*/
interface InitRuntimeInterface
{
/**
* Initializes the runtime environment.
*
* This is where you can load some file that contains filter functions for instance.
*/
public function initRuntime(Environment $environment);
}
class_alias('Twig\Extension\InitRuntimeInterface', 'Twig_Extension_InitRuntimeInterface');

View File

@@ -13,12 +13,9 @@ namespace Twig\Extension;
use Twig\NodeVisitor\OptimizerNodeVisitor;
/**
* @final
*/
class OptimizerExtension extends AbstractExtension
final class OptimizerExtension extends AbstractExtension
{
protected $optimizers;
private $optimizers;
public function __construct($optimizers = -1)
{
@@ -29,11 +26,6 @@ class OptimizerExtension extends AbstractExtension
{
return [new OptimizerNodeVisitor($this->optimizers)];
}
public function getName()
{
return 'optimizer';
}
}
class_alias('Twig\Extension\OptimizerExtension', 'Twig_Extension_Optimizer');

View File

@@ -41,12 +41,7 @@ class ProfilerExtension extends AbstractExtension
public function getNodeVisitors()
{
return [new ProfilerNodeVisitor(\get_class($this))];
}
public function getName()
{
return 'profiler';
return [new ProfilerNodeVisitor(static::class)];
}
}

View File

@@ -12,17 +12,17 @@
namespace Twig\Extension;
use Twig\NodeVisitor\SandboxNodeVisitor;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Source;
use Twig\TokenParser\SandboxTokenParser;
/**
* @final
*/
class SandboxExtension extends AbstractExtension
final class SandboxExtension extends AbstractExtension
{
protected $sandboxedGlobally;
protected $sandboxed;
protected $policy;
private $sandboxedGlobally;
private $sandboxed;
private $policy;
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false)
{
@@ -77,33 +77,49 @@ class SandboxExtension extends AbstractExtension
}
}
public function checkMethodAllowed($obj, $method)
public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null)
{
if ($this->isSandboxed()) {
$this->policy->checkMethodAllowed($obj, $method);
try {
$this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
}
}
public function checkPropertyAllowed($obj, $method)
public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null)
{
if ($this->isSandboxed()) {
$this->policy->checkPropertyAllowed($obj, $method);
try {
$this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
}
}
public function ensureToStringAllowed($obj)
public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null)
{
if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) {
$this->policy->checkMethodAllowed($obj, '__toString');
try {
$this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
}
return $obj;
}
public function getName()
{
return 'sandbox';
}
}
class_alias('Twig\Extension\SandboxExtension', 'Twig_Extension_Sandbox');

View File

@@ -13,32 +13,32 @@ namespace Twig\Extension;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/**
* Internal class.
*
* This class is used by \Twig\Environment as a staging area and must not be used directly.
* Used by \Twig\Environment as a staging area.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class StagingExtension extends AbstractExtension
final class StagingExtension extends AbstractExtension
{
protected $functions = [];
protected $filters = [];
protected $visitors = [];
protected $tokenParsers = [];
protected $globals = [];
protected $tests = [];
private $functions = [];
private $filters = [];
private $visitors = [];
private $tokenParsers = [];
private $tests = [];
public function addFunction($name, $function)
public function addFunction(TwigFunction $function)
{
if (isset($this->functions[$name])) {
@trigger_error(sprintf('Overriding function "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $name), E_USER_DEPRECATED);
if (isset($this->functions[$function->getName()])) {
throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName()));
}
$this->functions[$name] = $function;
$this->functions[$function->getName()] = $function;
}
public function getFunctions()
@@ -46,13 +46,13 @@ class StagingExtension extends AbstractExtension
return $this->functions;
}
public function addFilter($name, $filter)
public function addFilter(TwigFilter $filter)
{
if (isset($this->filters[$name])) {
@trigger_error(sprintf('Overriding filter "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $name), E_USER_DEPRECATED);
if (isset($this->filters[$filter->getName()])) {
throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName()));
}
$this->filters[$name] = $filter;
$this->filters[$filter->getName()] = $filter;
}
public function getFilters()
@@ -73,7 +73,7 @@ class StagingExtension extends AbstractExtension
public function addTokenParser(TokenParserInterface $parser)
{
if (isset($this->tokenParsers[$parser->getTag()])) {
@trigger_error(sprintf('Overriding tag "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $parser->getTag()), E_USER_DEPRECATED);
throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag()));
}
$this->tokenParsers[$parser->getTag()] = $parser;
@@ -84,34 +84,19 @@ class StagingExtension extends AbstractExtension
return $this->tokenParsers;
}
public function addGlobal($name, $value)
public function addTest(TwigTest $test)
{
$this->globals[$name] = $value;
}
public function getGlobals()
{
return $this->globals;
}
public function addTest($name, $test)
{
if (isset($this->tests[$name])) {
@trigger_error(sprintf('Overriding test "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $name), E_USER_DEPRECATED);
if (isset($this->tests[$test->getName()])) {
throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName()));
}
$this->tests[$name] = $test;
$this->tests[$test->getName()] = $test;
}
public function getTests()
{
return $this->tests;
}
public function getName()
{
return 'staging';
}
}
class_alias('Twig\Extension\StagingExtension', 'Twig_Extension_Staging');

View File

@@ -12,10 +12,7 @@
namespace Twig\Extension {
use Twig\TwigFunction;
/**
* @final
*/
class StringLoaderExtension extends AbstractExtension
final class StringLoaderExtension extends AbstractExtension
{
public function getFunctions()
{
@@ -23,11 +20,6 @@ class StringLoaderExtension extends AbstractExtension
new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]),
];
}
public function getName()
{
return 'string_loader';
}
}
class_alias('Twig\Extension\StringLoaderExtension', 'Twig_Extension_StringLoader');
@@ -47,7 +39,7 @@ use Twig\TemplateWrapper;
*
* @return TemplateWrapper
*/
function twig_template_from_string(Environment $env, $template, $name = null)
function twig_template_from_string(Environment $env, $template, string $name = null)
{
return $env->createTemplate((string) $template, $name);
}