This commit is contained in:
marco
2026-09-01 10:46:43 -03:00
parent bf96b77e9c
commit 34d33e1520
297 changed files with 14973 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
<?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\Profiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Profile implements \IteratorAggregate, \Serializable
{
public const ROOT = 'ROOT';
public const BLOCK = 'block';
public const TEMPLATE = 'template';
public const MACRO = 'macro';
private $starts = [];
private $ends = [];
private $profiles = [];
public function __construct(
private string $template = 'main',
private string $type = self::ROOT,
private string $name = 'main',
) {
$this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name;
$this->enter();
}
public function getTemplate(): string
{
return $this->template;
}
public function getType(): string
{
return $this->type;
}
public function getName(): string
{
return $this->name;
}
public function isRoot(): bool
{
return self::ROOT === $this->type;
}
public function isTemplate(): bool
{
return self::TEMPLATE === $this->type;
}
public function isBlock(): bool
{
return self::BLOCK === $this->type;
}
public function isMacro(): bool
{
return self::MACRO === $this->type;
}
/**
* @return Profile[]
*/
public function getProfiles(): array
{
return $this->profiles;
}
public function addProfile(self $profile): void
{
$this->profiles[] = $profile;
}
/**
* Returns the duration in microseconds.
*/
public function getDuration(): float
{
if ($this->isRoot() && $this->profiles) {
// for the root node with children, duration is the sum of all child durations
$duration = 0;
foreach ($this->profiles as $profile) {
$duration += $profile->getDuration();
}
return $duration;
}
return isset($this->ends['wt']) && isset($this->starts['wt']) ? $this->ends['wt'] - $this->starts['wt'] : 0;
}
/**
* Returns the start time in microseconds.
*/
public function getStartTime(): float
{
return $this->starts['wt'] ?? 0.0;
}
/**
* Returns the end time in microseconds.
*/
public function getEndTime(): float
{
return $this->ends['wt'] ?? 0.0;
}
/**
* Returns the memory usage in bytes.
*/
public function getMemoryUsage(): int
{
return isset($this->ends['mu']) && isset($this->starts['mu']) ? $this->ends['mu'] - $this->starts['mu'] : 0;
}
/**
* Returns the peak memory usage in bytes.
*/
public function getPeakMemoryUsage(): int
{
return isset($this->ends['pmu']) && isset($this->starts['pmu']) ? $this->ends['pmu'] - $this->starts['pmu'] : 0;
}
/**
* Starts the profiling.
*/
public function enter(): void
{
$this->starts = [
'wt' => microtime(true),
'mu' => memory_get_usage(),
'pmu' => memory_get_peak_usage(),
];
}
/**
* Stops the profiling.
*/
public function leave(): void
{
$this->ends = [
'wt' => microtime(true),
'mu' => memory_get_usage(),
'pmu' => memory_get_peak_usage(),
];
}
public function reset(): void
{
$this->starts = $this->ends = $this->profiles = [];
$this->enter();
}
public function getIterator(): \Traversable
{
return new \ArrayIterator($this->profiles);
}
public function serialize(): string
{
return serialize($this->__serialize());
}
public function unserialize($data): void
{
$this->__unserialize(unserialize($data));
}
/**
* @internal
*/
public function __serialize(): array
{
return [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles];
}
/**
* @internal
*/
public function __unserialize(array $data): void
{
[$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles] = $data;
}
}
+541
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);
}
+25
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.
*/
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);
}
+51
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);
}
+26
View File
@@ -0,0 +1,26 @@
<?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\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);
}
+340
View File
@@ -0,0 +1,340 @@
<?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> */
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
*
* @return void
*/
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
*
* @return void
*/
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
*
* @return void
*/
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 = $string::class;
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'], true)) {
// 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
if ('UTF-8' === $charset) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
}
// 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.
*/
$short = match ($char) {
'\\' => '\\\\',
'/' => '\\/',
"\x08" => '\b',
"\x0C" => '\f',
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
default => false,
};
if ($short) {
return $short;
}
$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[0]);
/*
* 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
*/
return match ($ord) {
34 => '&quot;', /* quotation mark */
38 => '&amp;', /* ampersand */
60 => '&lt;', /* less-than sign */
62 => '&gt;', /* greater-than sign */
default => \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);
}
}
+35
View File
@@ -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\RuntimeLoader;
use Psr\Container\ContainerInterface;
/**
* Lazily loads Twig runtime implementations from a PSR-11 container.
*
* Note that the runtime services MUST use their class names as identifiers.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ContainerRuntimeLoader implements RuntimeLoaderInterface
{
public function __construct(
private ContainerInterface $container,
) {
}
public function load(string $class)
{
return $this->container->has($class) ? $this->container->get($class) : null;
}
}
+39
View File
@@ -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\RuntimeLoader;
/**
* Lazy loads the runtime implementations for a Twig element.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class FactoryRuntimeLoader implements RuntimeLoaderInterface
{
/**
* @param array $map An array where keys are class names and values factory callables
*/
public function __construct(
private array $map = [],
) {
}
public function load(string $class)
{
if (!isset($this->map[$class])) {
return null;
}
$runtimeFactory = $this->map[$class];
return $runtimeFactory();
}
}
+27
View File
@@ -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\RuntimeLoader;
/**
* Creates runtime implementations for Twig elements (filters/functions/tests).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface RuntimeLoaderInterface
{
/**
* Creates the runtime implementation of a Twig element (filter/function/test).
*
* @return object|null The runtime instance or null if the loader does not know how to create the runtime for this class
*/
public function load(string $class);
}
+23
View File
@@ -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\Sandbox;
use Twig\Error\Error;
/**
* Exception thrown when a security error occurs at runtime.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class SecurityError extends Error
{
}
+33
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\Sandbox;
/**
* Exception thrown when a not allowed filter is used in a template.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
final class SecurityNotAllowedFilterError extends SecurityError
{
private string $filterName;
public function __construct(string $message, string $functionName)
{
parent::__construct($message);
$this->filterName = $functionName;
}
public function getFilterName(): string
{
return $this->filterName;
}
}
+33
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\Sandbox;
/**
* Exception thrown when a not allowed function is used in a template.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
final class SecurityNotAllowedFunctionError extends SecurityError
{
private string $functionName;
public function __construct(string $message, string $functionName)
{
parent::__construct($message);
$this->functionName = $functionName;
}
public function getFunctionName(): string
{
return $this->functionName;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Exception thrown when a not allowed class method is used in a template.
*
* @author Kit Burton-Senior <mail@kitbs.com>
*/
final class SecurityNotAllowedMethodError extends SecurityError
{
private string $className;
private string $methodName;
public function __construct(string $message, string $className, string $methodName)
{
parent::__construct($message);
$this->className = $className;
$this->methodName = $methodName;
}
public function getClassName(): string
{
return $this->className;
}
public function getMethodName(): string
{
return $this->methodName;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Exception thrown when a not allowed class property is used in a template.
*
* @author Kit Burton-Senior <mail@kitbs.com>
*/
final class SecurityNotAllowedPropertyError extends SecurityError
{
private string $className;
private string $propertyName;
public function __construct(string $message, string $className, string $propertyName)
{
parent::__construct($message);
$this->className = $className;
$this->propertyName = $propertyName;
}
public function getClassName(): string
{
return $this->className;
}
public function getPropertyName(): string
{
return $this->propertyName;
}
}
+33
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\Sandbox;
/**
* Exception thrown when a not allowed tag is used in a template.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
final class SecurityNotAllowedTagError extends SecurityError
{
private string $tagName;
public function __construct(string $message, string $tagName)
{
parent::__construct($message);
$this->tagName = $tagName;
}
public function getTagName(): string
{
return $this->tagName;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?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\Markup;
use Twig\Template;
/**
* Represents a security policy which need to be enforced when sandbox mode is enabled.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class SecurityPolicy implements SecurityPolicyInterface
{
private $allowedTags;
private $allowedFilters;
private $allowedMethods;
private $allowedProperties;
private $allowedFunctions;
public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [])
{
$this->allowedTags = $allowedTags;
$this->allowedFilters = $allowedFilters;
$this->setAllowedMethods($allowedMethods);
$this->allowedProperties = $allowedProperties;
$this->allowedFunctions = $allowedFunctions;
}
public function setAllowedTags(array $tags): void
{
$this->allowedTags = $tags;
}
public function setAllowedFilters(array $filters): void
{
$this->allowedFilters = $filters;
}
public function setAllowedMethods(array $methods): void
{
$this->allowedMethods = [];
foreach ($methods as $class => $m) {
$this->allowedMethods[$class] = array_map('strtolower', \is_array($m) ? $m : [$m]);
}
}
public function setAllowedProperties(array $properties): void
{
$this->allowedProperties = $properties;
}
public function setAllowedFunctions(array $functions): void
{
$this->allowedFunctions = $functions;
}
public function checkSecurity($tags, $filters, $functions): void
{
foreach ($tags as $tag) {
if (!\in_array($tag, $this->allowedTags, true)) {
if ('extends' === $tag) {
trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
} elseif ('use' === $tag) {
trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
} else {
throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
}
}
}
foreach ($filters as $filter) {
if (!\in_array($filter, $this->allowedFilters, true)) {
throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter);
}
}
foreach ($functions as $function) {
if (!\in_array($function, $this->allowedFunctions, true)) {
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
}
}
}
public function checkMethodAllowed($obj, $method): void
{
if ($obj instanceof Template || $obj instanceof Markup) {
return;
}
$allowed = false;
$method = strtolower($method);
foreach ($this->allowedMethods as $class => $methods) {
if ($obj instanceof $class && \in_array($method, $methods, true)) {
$allowed = true;
break;
}
}
if (!$allowed) {
$class = $obj::class;
throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
}
}
public function checkPropertyAllowed($obj, $property): void
{
$allowed = false;
foreach ($this->allowedProperties as $class => $properties) {
if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties], true)) {
$allowed = true;
break;
}
}
if (!$allowed) {
$class = $obj::class;
throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
}
}
}
+45
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\Sandbox;
/**
* Interface that all security policy classes must implements.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface SecurityPolicyInterface
{
/**
* @param string[] $tags
* @param string[] $filters
* @param string[] $functions
*
* @throws SecurityError
*/
public function checkSecurity($tags, $filters, $functions): void;
/**
* @param object $obj
* @param string $method
*
* @throws SecurityNotAllowedMethodError
*/
public function checkMethodAllowed($obj, $method): void;
/**
* @param object $obj
* @param string $property
*
* @throws SecurityNotAllowedPropertyError
*/
public function checkPropertyAllowed($obj, $property): void;
}
+24
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;
}
+47
View File
@@ -0,0 +1,47 @@
<?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;
/**
* Holds information about a non-compiled Twig template.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Source
{
/**
* @param string $code The template source code
* @param string $name The template logical name
* @param string $path The filesystem path of the template if any
*/
public function __construct(
private string $code,
private string $name,
private string $path = '',
) {
}
public function getCode(): string
{
return $this->code;
}
public function getName(): string
{
return $this->name;
}
public function getPath(): string
{
return $this->path;
}
}
+535
View File
@@ -0,0 +1,535 @@
<?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;
use Twig\Error\Error;
use Twig\Error\RuntimeError;
/**
* Default base class for compiled templates.
*
* This class is an implementation detail of how template compilation currently
* works, which might change. It should never be used directly. Use $twig->load()
* instead, which returns an instance of \Twig\TemplateWrapper.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
abstract class Template
{
public const ANY_CALL = 'any';
public const ARRAY_CALL = 'array';
public const METHOD_CALL = 'method';
protected $parent;
protected $parents = [];
protected $blocks = [];
protected $traits = [];
protected $traitAliases = [];
protected $extensions = [];
protected $sandbox;
private $useYield;
public function __construct(
protected Environment $env,
) {
$this->useYield = $env->useYield();
$this->extensions = $env->getExtensions();
}
/**
* Returns the template name.
*/
abstract public function getTemplateName(): string;
/**
* Returns debug information about the template.
*
* @return array<int, int> Debug information
*/
abstract public function getDebugInfo(): array;
/**
* Returns information about the original template source code.
*/
abstract public function getSourceContext(): Source;
/**
* Returns the parent template.
*
* This method is for internal use only and should never be called
* directly.
*
* @return self|TemplateWrapper|false The parent template or false if there is no parent
*/
public function getParent(array $context): self|TemplateWrapper|false
{
if (null !== $this->parent) {
return $this->parent;
}
if (!$parent = $this->doGetParent($context)) {
return false;
}
if ($parent instanceof self || $parent instanceof TemplateWrapper) {
return $this->parents[$parent->getSourceContext()->getName()] = $parent;
}
if (!isset($this->parents[$parent])) {
$this->parents[$parent] = $this->load($parent, -1);
}
return $this->parents[$parent];
}
protected function doGetParent(array $context): bool|string|self|TemplateWrapper
{
return false;
}
public function isTraitable(): bool
{
return true;
}
/**
* Displays a parent block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to display from the parent
* @param array $context The context
* @param array $blocks The current set of blocks
*/
public function displayParentBlock($name, array $context, array $blocks = []): void
{
foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
echo $data;
}
}
/**
* Displays a block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to display
* @param array $context The context
* @param array $blocks The current set of blocks
* @param bool $useBlocks Whether to use the current set of blocks
*/
public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void
{
foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) {
echo $data;
}
}
/**
* Renders a parent block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to render from the parent
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return string The rendered block
*/
public function renderParentBlock($name, array $context, array $blocks = []): string
{
if (!$this->useYield) {
if ($this->env->isDebug()) {
ob_start();
} else {
ob_start(function () { return ''; });
}
$this->displayParentBlock($name, $context, $blocks);
return ob_get_clean();
}
$content = '';
foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
$content .= $data;
}
return $content;
}
/**
* Renders a block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to render
* @param array $context The context
* @param array $blocks The current set of blocks
* @param bool $useBlocks Whether to use the current set of blocks
*
* @return string The rendered block
*/
public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string
{
if (!$this->useYield) {
$level = ob_get_level();
if ($this->env->isDebug()) {
ob_start();
} else {
ob_start(function () { return ''; });
}
try {
$this->displayBlock($name, $context, $blocks, $useBlocks);
} catch (\Throwable $e) {
while (ob_get_level() > $level) {
ob_end_clean();
}
throw $e;
}
return ob_get_clean();
}
$content = '';
foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) {
$content .= $data;
}
return $content;
}
/**
* Returns whether a block exists or not in the current context of the template.
*
* This method checks blocks defined in the current template
* or defined in "used" traits or defined in parent templates.
*
* @param string $name The block name
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return bool true if the block exists, false otherwise
*/
public function hasBlock($name, array $context, array $blocks = []): bool
{
if (isset($blocks[$name])) {
return $blocks[$name][0] instanceof self;
}
if (isset($this->blocks[$name])) {
return true;
}
if ($parent = $this->getParent($context)) {
return $parent->hasBlock($name, $context);
}
return false;
}
/**
* Returns all block names in the current context of the template.
*
* This method checks blocks defined in the current template
* or defined in "used" traits or defined in parent templates.
*
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return array<string> An array of block names
*/
public function getBlockNames(array $context, array $blocks = []): array
{
$names = array_merge(array_keys($blocks), array_keys($this->blocks));
if ($parent = $this->getParent($context)) {
$names = array_merge($names, $parent->getBlockNames($context));
}
return array_unique($names);
}
/**
* @param string|TemplateWrapper|array<string|TemplateWrapper> $template
*/
protected function load(string|TemplateWrapper|array $template, int $line, ?int $index = null): self
{
try {
if (\is_array($template)) {
return $this->env->resolveTemplate($template)->unwrap();
}
if ($template instanceof TemplateWrapper) {
return $template->unwrap();
}
if ($template === $this->getTemplateName()) {
$class = static::class;
if (false !== $pos = strrpos($class, '___', -1)) {
$class = substr($class, 0, $pos);
}
} else {
$class = $this->env->getTemplateClass($template);
}
return $this->env->loadTemplate($class, $template, $index);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($this->getSourceContext());
}
if ($e->getTemplateLine() > 0) {
throw $e;
}
if (-1 === $line) {
$e->guess();
} else {
$e->setTemplateLine($line);
}
throw $e;
}
}
/**
* @param string|TemplateWrapper|array<string|TemplateWrapper> $template
*
* @deprecated since Twig 3.21 and will be removed in 4.0. Use Template::load() instead.
*/
protected function loadTemplate($template, $templateName = null, ?int $line = null, ?int $index = null): self|TemplateWrapper
{
trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__);
if (null === $line) {
$line = -1;
}
if ($template instanceof self) {
return $template;
}
return $this->load($template, $line, $index);
}
/**
* @internal
*
* @return $this
*/
public function unwrap(): self
{
return $this;
}
/**
* Returns all blocks.
*
* This method is for internal use only and should never be called
* directly.
*
* @return array An array of blocks
*/
public function getBlocks(): array
{
return $this->blocks;
}
public function display(array $context, array $blocks = []): void
{
foreach ($this->yield($context, $blocks) as $data) {
echo $data;
}
}
public function render(array $context): string
{
if (!$this->useYield) {
$level = ob_get_level();
if ($this->env->isDebug()) {
ob_start();
} else {
ob_start(function () { return ''; });
}
try {
$this->display($context);
} catch (\Throwable $e) {
while (ob_get_level() > $level) {
ob_end_clean();
}
throw $e;
}
return ob_get_clean();
}
$content = '';
foreach ($this->yield($context) as $data) {
$content .= $data;
}
return $content;
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function yield(array $context, array $blocks = []): iterable
{
$context += $this->env->getGlobals();
$blocks = array_merge($this->blocks, $blocks);
try {
yield from $this->doDisplay($context, $blocks);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($this->getSourceContext());
}
// this is mostly useful for \Twig\Error\LoaderError exceptions
// see \Twig\Error\LoaderError
if (-1 === $e->getTemplateLine()) {
$e->guess();
}
throw $e;
} catch (\Throwable $e) {
$e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
$e->guess();
throw $e;
}
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable
{
if ($useBlocks && isset($blocks[$name])) {
$template = $blocks[$name][0];
$block = $blocks[$name][1];
} elseif (isset($this->blocks[$name])) {
$template = $this->blocks[$name][0];
$block = $this->blocks[$name][1];
} else {
$template = null;
$block = null;
}
// avoid RCEs when sandbox is enabled
if (null !== $template && !$template instanceof self) {
throw new \LogicException('A block must be a method on a \Twig\Template instance.');
}
if (null !== $template) {
try {
yield from $template->$block($context, $blocks);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($template->getSourceContext());
}
// this is mostly useful for \Twig\Error\LoaderError exceptions
// see \Twig\Error\LoaderError
if (-1 === $e->getTemplateLine()) {
$e->guess();
}
throw $e;
} catch (\Throwable $e) {
$e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
$e->guess();
throw $e;
}
} elseif ($parent = $this->getParent($context)) {
yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
} elseif (isset($blocks[$name])) {
throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext());
} else {
throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
}
}
/**
* Yields a parent block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to display from the parent
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return iterable<scalar|\Stringable|null>
*/
public function yieldParentBlock($name, array $context, array $blocks = []): iterable
{
if (isset($this->traits[$name])) {
yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false);
} elseif ($parent = $this->getParent($context)) {
yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false);
} else {
throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext());
}
}
protected function hasMacro(string $name, array $context): bool
{
if (method_exists($this, $name)) {
return true;
}
if (!$parent = $this->getParent($context)) {
return false;
}
return $parent->hasMacro($name, $context);
}
protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self
{
if (method_exists($this, $name)) {
return $this;
}
$parent = $this;
while ($parent = $parent->getParent($context)) {
if (method_exists($parent, $name)) {
return $parent;
}
}
throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source);
}
/**
* Auto-generated method to display the template with the given context.
*
* @param array $context An array of parameters to pass to the template
* @param array $blocks An array of blocks to pass to the template
*
* @return iterable<scalar|\Stringable|null>
*/
abstract protected function doDisplay(array $context, array $blocks = []): iterable;
}
+112
View File
@@ -0,0 +1,112 @@
<?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;
/**
* Exposes a template to userland.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TemplateWrapper
{
/**
* This method is for internal use only and should never be called
* directly (use Twig\Environment::load() instead).
*
* @internal
*/
public function __construct(
private Environment $env,
private Template $template,
) {
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function stream(array $context = []): iterable
{
yield from $this->template->yield($context);
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function streamBlock(string $name, array $context = []): iterable
{
yield from $this->template->yieldBlock($name, $context);
}
public function render(array $context = []): string
{
return $this->template->render($context);
}
/**
* @return void
*/
public function display(array $context = [])
{
// using func_get_args() allows to not expose the blocks argument
// as it should only be used by internal code
$this->template->display($context, \func_get_args()[1] ?? []);
}
public function hasBlock(string $name, array $context = []): bool
{
return $this->template->hasBlock($name, $context);
}
/**
* @return string[] An array of defined template block names
*/
public function getBlockNames(array $context = []): array
{
return $this->template->getBlockNames($context);
}
public function renderBlock(string $name, array $context = []): string
{
return $this->template->renderBlock($name, $context + $this->env->getGlobals());
}
/**
* @return void
*/
public function displayBlock(string $name, array $context = [])
{
$context += $this->env->getGlobals();
foreach ($this->template->yieldBlock($name, $context) as $data) {
echo $data;
}
}
public function getSourceContext(): Source
{
return $this->template->getSourceContext();
}
public function getTemplateName(): string
{
return $this->template->getTemplateName();
}
/**
* @internal
*
* @return Template
*/
public function unwrap()
{
return $this->template;
}
}
+351
View File
@@ -0,0 +1,351 @@
<?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\Test;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Error\Error;
use Twig\Extension\ExtensionInterface;
use Twig\Loader\ArrayLoader;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/**
* Integration test helper.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Karma Dordrak <drak@zikula.org>
*/
abstract class IntegrationTestCase extends TestCase
{
/**
* @deprecated since Twig 3.13, use getFixturesDirectory() instead.
*
* @return string
*/
protected function getFixturesDir()
{
throw new \BadMethodCallException('Not implemented.');
}
protected static function getFixturesDirectory(): string
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* @return RuntimeLoaderInterface[]
*/
protected function getRuntimeLoaders()
{
return [];
}
/**
* @return ExtensionInterface[]
*/
protected function getExtensions()
{
return [];
}
/**
* @return TwigFilter[]
*/
protected function getTwigFilters()
{
return [];
}
/**
* @return TwigFunction[]
*/
protected function getTwigFunctions()
{
return [];
}
/**
* @return TwigTest[]
*/
protected function getTwigTests()
{
return [];
}
/**
* @return array<callable(string): (TwigFilter|false)>
*/
protected function getUndefinedFilterCallbacks(): array
{
return [];
}
/**
* @return array<callable(string): (TwigFunction|false)>
*/
protected function getUndefinedFunctionCallbacks(): array
{
return [];
}
/**
* @return array<callable(string): (TwigTest|false)>
*/
protected function getUndefinedTestCallbacks(): array
{
return [];
}
/**
* @return array<callable(string): (TokenParserInterface|false)>
*/
protected function getUndefinedTokenParserCallbacks(): array
{
return [];
}
/**
* @dataProvider getTests
*
* @return void
*/
public function testIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '')
{
$this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation);
}
/**
* @dataProvider getLegacyTests
*
* @group legacy
*
* @return void
*/
public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '')
{
$this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation);
}
/**
* @return iterable
*
* @final since Twig 3.13
*/
public function getTests($name, $legacyTests = false)
{
try {
$fixturesDir = static::getFixturesDirectory();
} catch (\BadMethodCallException) {
trigger_deprecation('twig/twig', '3.13', 'Not overriding "%s::getFixturesDirectory()" in "%s" is deprecated. This method will be abstract in 4.0.', self::class, static::class);
$fixturesDir = $this->getFixturesDir();
}
$fixturesDir = realpath($fixturesDir);
$tests = [];
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
if (!preg_match('/\.test$/', $file)) {
continue;
}
if ($legacyTests xor str_contains($file->getRealpath(), '.legacy.test')) {
continue;
}
$test = file_get_contents($file->getRealpath());
if (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*(?:--DEPRECATION--\s*(.*?))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)\s*(?:--DATA--\s*(.*))?\s*--EXCEPTION--\s*(.*)/sx', $test, $match)) {
$message = $match[1];
$condition = $match[2];
$deprecation = $match[3];
$templates = self::parseTemplates($match[4]);
$exception = $match[6];
$outputs = [[null, $match[5], null, '']];
} elseif (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*(?:--DEPRECATION--\s*(.*?))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)--DATA--.*?--EXPECT--.*/s', $test, $match)) {
$message = $match[1];
$condition = $match[2];
$deprecation = $match[3];
$templates = self::parseTemplates($match[4]);
$exception = false;
preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, \PREG_SET_ORDER);
} else {
throw new \InvalidArgumentException(\sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file)));
}
$tests[str_replace($fixturesDir.'/', '', $file)] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation];
}
if ($legacyTests && !$tests) {
// add a dummy test to avoid a PHPUnit message
return [['not', '-', '', [], '', []]];
}
return $tests;
}
/**
* @final since Twig 3.13
*
* @return iterable
*/
public function getLegacyTests()
{
return $this->getTests('testLegacyIntegration', true);
}
/**
* @return void
*/
protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '')
{
if (!$outputs) {
$this->markTestSkipped('no tests to run');
}
if ($condition) {
$ret = '';
eval('$ret = '.$condition.';');
if (!$ret) {
$this->markTestSkipped($condition);
}
}
foreach ($outputs as $i => $match) {
$config = array_merge([
'cache' => false,
'strict_variables' => true,
], $match[2] ? eval($match[2].';') : []);
// make sure that template are always compiled even if they are the same (useful when testing with more than one data/expect sections)
foreach ($templates as $j => $template) {
$templates[$j] = $template.str_repeat(' ', $i);
}
$loader = new ArrayLoader($templates);
$twig = new Environment($loader, $config);
$twig->addGlobal('global', 'global');
foreach ($this->getRuntimeLoaders() as $runtimeLoader) {
$twig->addRuntimeLoader($runtimeLoader);
}
foreach ($this->getExtensions() as $extension) {
$twig->addExtension($extension);
}
foreach ($this->getTwigFilters() as $filter) {
$twig->addFilter($filter);
}
foreach ($this->getTwigTests() as $test) {
$twig->addTest($test);
}
foreach ($this->getTwigFunctions() as $function) {
$twig->addFunction($function);
}
foreach ($this->getUndefinedFilterCallbacks() as $callback) {
$twig->registerUndefinedFilterCallback($callback);
}
foreach ($this->getUndefinedFunctionCallbacks() as $callback) {
$twig->registerUndefinedFunctionCallback($callback);
}
foreach ($this->getUndefinedTestCallbacks() as $callback) {
$twig->registerUndefinedTestCallback($callback);
}
foreach ($this->getUndefinedTokenParserCallbacks() as $callback) {
$twig->registerUndefinedTokenParserCallback($callback);
}
$deprecations = [];
try {
$prevHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$prevHandler) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
return true;
}
return $prevHandler ? $prevHandler($type, $msg, $file, $line, $context) : false;
});
$template = $twig->load('index.twig');
} catch (\Exception $e) {
if (false !== $exception) {
$message = $e->getMessage();
$this->assertSame(trim($exception), trim(\sprintf('%s: %s', $e::class, $message)));
$last = substr($message, \strlen($message) - 1);
$this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.');
return;
}
throw new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e);
} finally {
restore_error_handler();
}
$this->assertSame($deprecation, implode("\n", $deprecations));
try {
$output = trim($template->render(eval($match[1].';')), "\n ");
} catch (\Exception $e) {
if (false !== $exception) {
$this->assertStringMatchesFormat(trim($exception), trim(\sprintf('%s: %s', $e::class, $e->getMessage())));
return;
}
$e = new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e);
$output = trim(\sprintf('%s: %s', $e::class, $e->getMessage()));
}
if (false !== $exception) {
[$class] = explode(':', $exception);
$constraintClass = class_exists('PHPUnit\Framework\Constraint\Exception') ? 'PHPUnit\Framework\Constraint\Exception' : 'PHPUnit_Framework_Constraint_Exception';
$this->assertThat(null, new $constraintClass($class));
}
$expected = trim($match[3], "\n ");
if ($expected !== $output) {
printf("Compiled templates that failed on case %d:\n", $i + 1);
foreach (array_keys($templates) as $name) {
echo "Template: $name\n";
echo $twig->compile($twig->parse($twig->tokenize($twig->getLoader()->getSourceContext($name))));
}
}
$this->assertEquals($expected, $output, $message.' (in '.$file.')');
}
}
/**
* @return array<string, string>
*/
protected static function parseTemplates($test)
{
$templates = [];
preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, \PREG_SET_ORDER);
foreach ($matches as $match) {
$templates[$match[1] ?: 'index.twig'] = $match[2];
}
return $templates;
}
}
+142
View File
@@ -0,0 +1,142 @@
<?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\Test;
use PHPUnit\Framework\Attributes\BeforeClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Twig\Compiler;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\Node\Node;
abstract class NodeTestCase extends TestCase
{
/**
* @var Environment
*/
private $currentEnv;
/**
* @return iterable<array{0: Node, 1: string, 2?: Environment|null, 3?: bool}>
*/
public function getTests()
{
return [];
}
/**
* @return iterable<array{0: Node, 1: string, 2?: Environment|null, 3?: bool}>
*/
public static function provideTests(): iterable
{
trigger_deprecation('twig/twig', '3.13', 'Not implementing "%s()" in "%s" is deprecated. This method will be abstract in 4.0.', __METHOD__, static::class);
return [];
}
/**
* @dataProvider getTests
* @dataProvider provideTests
*
* @return void
*/
#[DataProvider('getTests'), DataProvider('provideTests')]
public function testCompile($node, $source, $environment = null, $isPattern = false)
{
$this->assertNodeCompilation($source, $node, $environment, $isPattern);
}
/**
* @return void
*/
public function assertNodeCompilation($source, Node $node, ?Environment $environment = null, $isPattern = false)
{
$compiler = $this->getCompiler($environment);
$compiler->compile($node);
if ($isPattern) {
$this->assertStringMatchesFormat($source, trim($compiler->getSource()));
} else {
$this->assertEquals($source, trim($compiler->getSource()));
}
}
/**
* @return Compiler
*/
protected function getCompiler(?Environment $environment = null)
{
return new Compiler($environment ?? $this->getEnvironment());
}
/**
* @return Environment
*
* @final since Twig 3.13
*/
protected function getEnvironment()
{
return $this->currentEnv ??= static::createEnvironment();
}
protected static function createEnvironment(): Environment
{
return new Environment(new ArrayLoader());
}
/**
* @return string
*
* @deprecated since Twig 3.13, use createVariableGetter() instead.
*/
protected function getVariableGetter($name, $line = false)
{
trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createVariableGetter()" instead.', __METHOD__);
return self::createVariableGetter($name, $line);
}
final protected static function createVariableGetter(string $name, bool $line = false): string
{
$line = $line > 0 ? "// line $line\n" : '';
return \sprintf('%s($context["%s"] ?? null)', $line, $name);
}
/**
* @return string
*
* @deprecated since Twig 3.13, use createAttributeGetter() instead.
*/
protected function getAttributeGetter()
{
trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createAttributeGetter()" instead.', __METHOD__);
return self::createAttributeGetter();
}
final protected static function createAttributeGetter(): string
{
return 'CoreExtension::getAttribute($this->env, $this->source, ';
}
/** @beforeClass */
#[BeforeClass]
final public static function checkDataProvider(): void
{
$r = new \ReflectionMethod(static::class, 'getTests');
if (self::class !== $r->getDeclaringClass()->getName()) {
trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class);
}
}
}
+239
View File
@@ -0,0 +1,239 @@
<?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;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Token
{
public const EOF_TYPE = -1;
public const TEXT_TYPE = 0;
public const BLOCK_START_TYPE = 1;
public const VAR_START_TYPE = 2;
public const BLOCK_END_TYPE = 3;
public const VAR_END_TYPE = 4;
public const NAME_TYPE = 5;
public const NUMBER_TYPE = 6;
public const STRING_TYPE = 7;
public const OPERATOR_TYPE = 8;
public const PUNCTUATION_TYPE = 9;
public const INTERPOLATION_START_TYPE = 10;
public const INTERPOLATION_END_TYPE = 11;
/**
* @deprecated since Twig 3.21, "arrow" is now an operator
*/
public const ARROW_TYPE = 12;
/**
* @deprecated since Twig 3.21, "spread" is now an operator
*/
public const SPREAD_TYPE = 13;
public function __construct(
private int $type,
private $value,
private int $lineno,
) {
if (self::ARROW_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE);
}
if (self::SPREAD_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::SPREAD_TYPE);
}
}
public function __toString(): string
{
return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value);
}
/**
* Tests the current token for a type and/or a value.
*
* Parameters may be:
* * just type
* * type and value (or array of possible values)
* * just value (or array of possible values) (NAME_TYPE is used as type)
*
* @param array|string|int $type The type to test
* @param array|string|null $values The token value
*/
public function test($type, $values = null): bool
{
if (null === $values && !\is_int($type)) {
$values = $type;
$type = self::NAME_TYPE;
}
if (self::ARROW_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::typeToEnglish(self::ARROW_TYPE));
return self::OPERATOR_TYPE === $this->type && '=>' === $this->value;
}
if (self::SPREAD_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::typeToEnglish(self::SPREAD_TYPE));
return self::OPERATOR_TYPE === $this->type && '...' === $this->value;
}
$typeMatches = $this->type === $type;
if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:'], true) && $values) {
foreach ((array) $values as $value) {
if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish());
break;
}
}
}
if (!$typeMatches) {
if (self::OPERATOR_TYPE === $type && self::PUNCTUATION_TYPE === $this->type) {
if ($values) {
foreach ((array) $values as $value) {
if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) {
$typeMatches = true;
break;
}
}
} else {
$typeMatches = true;
}
}
}
return $typeMatches && (
null === $values
|| (\is_array($values) && \in_array($this->value, $values, true))
|| $this->value == $values
);
}
public function getLine(): int
{
return $this->lineno;
}
/**
* @deprecated since Twig 3.19
*/
public function getType(): int
{
trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated.', __METHOD__));
return $this->type;
}
public function getValue()
{
return $this->value;
}
public function toEnglish(): string
{
return self::typeToEnglish($this->type);
}
public static function typeToString(int $type, bool $short = false): string
{
switch ($type) {
case self::EOF_TYPE:
$name = 'EOF_TYPE';
break;
case self::TEXT_TYPE:
$name = 'TEXT_TYPE';
break;
case self::BLOCK_START_TYPE:
$name = 'BLOCK_START_TYPE';
break;
case self::VAR_START_TYPE:
$name = 'VAR_START_TYPE';
break;
case self::BLOCK_END_TYPE:
$name = 'BLOCK_END_TYPE';
break;
case self::VAR_END_TYPE:
$name = 'VAR_END_TYPE';
break;
case self::NAME_TYPE:
$name = 'NAME_TYPE';
break;
case self::NUMBER_TYPE:
$name = 'NUMBER_TYPE';
break;
case self::STRING_TYPE:
$name = 'STRING_TYPE';
break;
case self::OPERATOR_TYPE:
$name = 'OPERATOR_TYPE';
break;
case self::PUNCTUATION_TYPE:
$name = 'PUNCTUATION_TYPE';
break;
case self::INTERPOLATION_START_TYPE:
$name = 'INTERPOLATION_START_TYPE';
break;
case self::INTERPOLATION_END_TYPE:
$name = 'INTERPOLATION_END_TYPE';
break;
case self::ARROW_TYPE:
$name = 'ARROW_TYPE';
break;
case self::SPREAD_TYPE:
$name = 'SPREAD_TYPE';
break;
default:
throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type));
}
return $short ? $name : 'Twig\Token::'.$name;
}
public static function typeToEnglish(int $type): string
{
switch ($type) {
case self::EOF_TYPE:
return 'end of template';
case self::TEXT_TYPE:
return 'text';
case self::BLOCK_START_TYPE:
return 'begin of statement block';
case self::VAR_START_TYPE:
return 'begin of print statement';
case self::BLOCK_END_TYPE:
return 'end of statement block';
case self::VAR_END_TYPE:
return 'end of print statement';
case self::NAME_TYPE:
return 'name';
case self::NUMBER_TYPE:
return 'number';
case self::STRING_TYPE:
return 'string';
case self::OPERATOR_TYPE:
return 'operator';
case self::PUNCTUATION_TYPE:
return 'punctuation';
case self::INTERPOLATION_START_TYPE:
return 'begin of string interpolation';
case self::INTERPOLATION_END_TYPE:
return 'end of string interpolation';
case self::ARROW_TYPE:
return 'arrow function';
case self::SPREAD_TYPE:
return 'spread operator';
default:
throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type));
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<?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\Lexer;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Nodes;
use Twig\Parser;
use Twig\Token;
/**
* Base class for all token parsers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractTokenParser implements TokenParserInterface
{
/**
* @var Parser
*/
protected $parser;
public function setParser(Parser $parser): void
{
$this->parser = $parser;
}
/**
* Parses an assignment expression like "a, b".
*/
protected function parseAssignmentExpression(): Nodes
{
$stream = $this->parser->getStream();
$targets = [];
while (true) {
$token = $stream->getCurrent();
if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
// in this context, string operators are variable names
$stream->next();
} else {
$stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
}
$targets[] = new AssignContextVariable($token->getValue(), $token->getLine());
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
return new Nodes($targets);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?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\ExpressionParser\Infix\FilterExpressionParser;
use Twig\Node\Expression\Variable\LocalVariable;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
use Twig\Token;
/**
* Applies filters on a section of a template.
*
* {% apply upper %}
* This text becomes uppercase
* {% endapply %}
*
* @internal
*/
final class ApplyTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$ref = new LocalVariable(null, $lineno);
$filter = $ref;
$op = $this->parser->getEnvironment()->getExpressionParsers()->getByClass(FilterExpressionParser::class);
while (true) {
$filter = $op->parse($this->parser, $filter, $this->parser->getCurrentToken());
if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) {
break;
}
$this->parser->getStream()->next();
}
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideApplyEnd'], true);
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new Nodes([
new SetNode(true, $ref, $body, $lineno),
new PrintNode($filter, $lineno),
], $lineno);
}
public function decideApplyEnd(Token $token): bool
{
return $token->test('endapply');
}
public function getTag(): string
{
return 'apply';
}
}
+58
View File
@@ -0,0 +1,58 @@
<?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\AutoEscapeNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
use Twig\Token;
/**
* Marks a section of a template to be escaped or not.
*
* @internal
*/
final class AutoEscapeTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
if ($stream->test(Token::BLOCK_END_TYPE)) {
$value = 'html';
} else {
$expr = $this->parser->parseExpression();
if (!$expr instanceof ConstantExpression) {
throw new SyntaxError('An escaping strategy must be a string or false.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
$value = $expr->getAttribute('value');
}
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
return new AutoEscapeNode($value, $body, $lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endautoescape');
}
public function getTag(): string
{
return 'autoescape';
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
use Twig\Node\EmptyNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Token;
/**
* Marks a section of a template as being reusable.
*
* {% block head %}
* <link rel="stylesheet" href="style.css" />
* <title>{% block title %}{% endblock %} - My Webpage</title>
* {% endblock %}
*
* @internal
*/
final class BlockTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue();
$this->parser->setBlock($name, $block = new BlockNode($name, new EmptyNode(), $lineno));
$this->parser->pushLocalScope();
$this->parser->pushBlockStack($name);
if ($stream->nextIf(Token::BLOCK_END_TYPE)) {
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$value = $token->getValue();
if ($value != $name) {
throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
} else {
$body = new Nodes([
new PrintNode($this->parser->parseExpression(), $lineno),
]);
}
$stream->expect(Token::BLOCK_END_TYPE);
$block->setNode('body', $body);
$this->parser->popBlockStack();
$this->parser->popLocalScope();
return new BlockReferenceNode($name, $lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endblock');
}
public function getTag(): string
{
return 'block';
}
}
+65
View File
@@ -0,0 +1,65 @@
<?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\DeprecatedNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Deprecates a section of a template.
*
* {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' %}
* {% extends 'layout.html.twig' %}
*
* {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package="foo/bar" version="1.1" %}
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
final class DeprecatedTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$expr = $this->parser->parseExpression();
$node = new DeprecatedNode($expr, $token->getLine());
while ($stream->test(Token::NAME_TYPE)) {
$k = $stream->getCurrent()->getValue();
$stream->next();
$stream->expect(Token::OPERATOR_TYPE, '=');
switch ($k) {
case 'package':
$node->setNode('package', $this->parser->parseExpression());
break;
case 'version':
$node->setNode('version', $this->parser->parseExpression());
break;
default:
throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
$stream->expect(Token::BLOCK_END_TYPE);
return $node;
}
public function getTag(): string
{
return 'deprecated';
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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\Node\DoNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Evaluates an expression, discarding the returned value.
*
* @internal
*/
final class DoTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$expr = $this->parser->parseExpression();
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new DoNode($expr, $token->getLine());
}
public function getTag(): string
{
return 'do';
}
}
+73
View File
@@ -0,0 +1,73 @@
<?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\Node\EmbedNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
use Twig\Token;
/**
* Embeds a template.
*
* @internal
*/
final class EmbedTokenParser extends IncludeTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$parent = $this->parser->parseExpression();
[$variables, $only, $ignoreMissing] = $this->parseArguments();
$parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine());
if ($parent instanceof ConstantExpression) {
$parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine());
} elseif ($parent instanceof ContextVariable) {
$parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine());
}
// inject a fake parent to make the parent() function work
$stream->injectTokens([
new Token(Token::BLOCK_START_TYPE, '', $token->getLine()),
new Token(Token::NAME_TYPE, 'extends', $token->getLine()),
$parentToken,
new Token(Token::BLOCK_END_TYPE, '', $token->getLine()),
]);
$module = $this->parser->parse($stream, [$this, 'decideBlockEnd'], true);
// override the parent with the correct one
if ($fakeParentToken === $parentToken) {
$module->setNode('parent', $parent);
}
$this->parser->embedTemplate($module);
$stream->expect(Token::BLOCK_END_TYPE);
return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine());
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endembed');
}
public function getTag(): string
{
return 'embed';
}
}
+50
View File
@@ -0,0 +1,50 @@
<?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\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\EmptyNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Extends a template by another one.
*
* {% extends "base.html" %}
*
* @internal
*/
final class ExtendsTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
if ($this->parser->peekBlockStack()) {
throw new SyntaxError('Cannot use "extend" in a block.', $token->getLine(), $stream->getSourceContext());
} elseif (!$this->parser->isMainScope()) {
throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext());
}
$this->parser->setParent($this->parser->parseExpression());
$stream->expect(Token::BLOCK_END_TYPE);
return new EmptyNode($token->getLine());
}
public function getTag(): string
{
return 'extends';
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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\Node\FlushNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Flushes the output to the client.
*
* @see flush()
*
* @internal
*/
final class FlushTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new FlushNode($token->getLine());
}
public function getTag(): string
{
return 'flush';
}
}
+80
View File
@@ -0,0 +1,80 @@
<?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\TokenParser;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\ForElseNode;
use Twig\Node\ForNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Loops over each item of a sequence.
*
* <ul>
* {% for user in users %}
* <li>{{ user.username|e }}</li>
* {% endfor %}
* </ul>
*
* @internal
*/
final class ForTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$targets = $this->parseAssignmentExpression();
$stream->expect(Token::OPERATOR_TYPE, 'in');
$seq = $this->parser->parseExpression();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideForFork']);
if ('else' == $stream->next()->getValue()) {
$elseLineno = $stream->getCurrent()->getLine();
$stream->expect(Token::BLOCK_END_TYPE);
$else = new ForElseNode($this->parser->subparse([$this, 'decideForEnd'], true), $elseLineno);
} else {
$else = null;
}
$stream->expect(Token::BLOCK_END_TYPE);
if (\count($targets) > 1) {
$keyTarget = $targets->getNode('0');
$keyTarget = new AssignContextVariable($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine());
$valueTarget = $targets->getNode('1');
} else {
$keyTarget = new AssignContextVariable('_key', $lineno);
$valueTarget = $targets->getNode('0');
}
$valueTarget = new AssignContextVariable($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine());
return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno);
}
public function decideForFork(Token $token): bool
{
return $token->test(['else', 'endfor']);
}
public function decideForEnd(Token $token): bool
{
return $token->test('endfor');
}
public function getTag(): string
{
return 'for';
}
}
+69
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\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\AssignTemplateVariable;
use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\ImportNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Imports macros.
*
* {% from 'forms.html.twig' import forms %}
*
* @internal
*/
final class FromTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$macro = $this->parser->parseExpression();
$stream = $this->parser->getStream();
$stream->expect(Token::NAME_TYPE, 'import');
$targets = [];
while (true) {
$name = $stream->expect(Token::NAME_TYPE)->getValue();
if ($stream->nextIf('as')) {
$alias = new AssignContextVariable($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine());
} else {
$alias = new AssignContextVariable($name, $token->getLine());
}
$targets[$name] = $alias;
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
$stream->expect(Token::BLOCK_END_TYPE);
$internalRef = new AssignTemplateVariable(new TemplateVariable(null, $token->getLine()), $this->parser->isMainScope());
$node = new ImportNode($macro, $internalRef, $token->getLine());
foreach ($targets as $name => $alias) {
$this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $internalRef);
}
return $node;
}
public function getTag(): string
{
return 'from';
}
}
+79
View File
@@ -0,0 +1,79 @@
<?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'], true)) {
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);
$name = $nameToken->getValue();
if ('test' === $typeToken->getValue() && $stream->test(Token::NAME_TYPE)) {
// try 2-words tests
$name .= ' '.$stream->getCurrent()->getValue();
$stream->next();
}
try {
$exists = null !== $this->parser->getEnvironment()->$method($name);
} catch (SyntaxError) {
$exists = false;
}
$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';
}
}
+90
View File
@@ -0,0 +1,90 @@
<?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\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\IfNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Token;
/**
* Tests a condition.
*
* {% if users %}
* <ul>
* {% for user in users %}
* <li>{{ user.username|e }}</li>
* {% endfor %}
* </ul>
* {% endif %}
*
* @internal
*/
final class IfTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$expr = $this->parser->parseExpression();
$stream = $this->parser->getStream();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideIfFork']);
$tests = [$expr, $body];
$else = null;
$end = false;
while (!$end) {
switch ($stream->next()->getValue()) {
case 'else':
$stream->expect(Token::BLOCK_END_TYPE);
$else = $this->parser->subparse([$this, 'decideIfEnd']);
break;
case 'elseif':
$expr = $this->parser->parseExpression();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideIfFork']);
$tests[] = $expr;
$tests[] = $body;
break;
case 'endif':
$end = true;
break;
default:
throw new SyntaxError(\sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
$stream->expect(Token::BLOCK_END_TYPE);
return new IfNode(new Nodes($tests), $else, $lineno);
}
public function decideIfFork(Token $token): bool
{
return $token->test(['elseif', 'else', 'endif']);
}
public function decideIfEnd(Token $token): bool
{
return $token->test(['endif']);
}
public function getTag(): string
{
return 'if';
}
}
+45
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\TokenParser;
use Twig\Node\Expression\Variable\AssignTemplateVariable;
use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\ImportNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Imports macros.
*
* {% import 'forms.html.twig' as forms %}
*
* @internal
*/
final class ImportTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$macro = $this->parser->parseExpression();
$this->parser->getStream()->expect(Token::NAME_TYPE, 'as');
$name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue();
$var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope());
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
$this->parser->addImportedSymbol('template', $name);
return new ImportNode($macro, $var, $token->getLine());
}
public function getTag(): string
{
return 'import';
}
}
+73
View File
@@ -0,0 +1,73 @@
<?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\TokenParser;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\IncludeNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Includes a template.
*
* {% include 'header.html.twig' %}
* Body
* {% include 'footer.html.twig' %}
*
* @internal
*/
class IncludeTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$expr = $this->parser->parseExpression();
[$variables, $only, $ignoreMissing] = $this->parseArguments();
return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine());
}
/**
* @return array{0: ?AbstractExpression, 1: bool, 2: bool}
*/
protected function parseArguments()
{
$stream = $this->parser->getStream();
$ignoreMissing = false;
if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) {
$stream->expect(Token::NAME_TYPE, 'missing');
$ignoreMissing = true;
}
$variables = null;
if ($stream->nextIf(Token::NAME_TYPE, 'with')) {
$variables = $this->parser->parseExpression();
}
$only = false;
if ($stream->nextIf(Token::NAME_TYPE, 'only')) {
$only = true;
}
$stream->expect(Token::BLOCK_END_TYPE);
return [$variables, $only, $ignoreMissing];
}
public function getTag(): string
{
return 'include';
}
}
+123
View File
@@ -0,0 +1,123 @@
<?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\BodyNode;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\PosUnary;
use Twig\Node\Expression\Variable\LocalVariable;
use Twig\Node\MacroNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Defines a macro.
*
* {% macro input(name, value, type, size) %}
* <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
* {% endmacro %}
*
* @internal
*/
final class MacroTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue();
$arguments = $this->parseDefinition();
$stream->expect(Token::BLOCK_END_TYPE);
$this->parser->pushLocalScope();
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$value = $token->getValue();
if ($value != $name) {
throw new SyntaxError(\sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
$this->parser->popLocalScope();
$stream->expect(Token::BLOCK_END_TYPE);
$this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno));
return new EmptyNode($lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endmacro');
}
public function getTag(): string
{
return 'macro';
}
private function parseDefinition(): ArrayExpression
{
$arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine());
$stream = $this->parser->getStream();
$stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
if (\count($arguments)) {
$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;
}
}
$token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
$name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine());
if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) {
$default = $this->parser->parseExpression();
} else {
$default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
$default->setAttribute('is_implicit', true);
}
if (!$this->checkConstantExpression($default)) {
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());
}
$arguments->addElement($default, $name);
}
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
return $arguments;
}
// checks that the node only contains "constant" elements
private function checkConstantExpression(Node $node): bool
{
if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
|| $node instanceof NegUnary || $node instanceof PosUnary
)) {
return false;
}
foreach ($node as $n) {
if (!$this->checkConstantExpression($n)) {
return false;
}
}
return true;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?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\IncludeNode;
use Twig\Node\Node;
use Twig\Node\SandboxNode;
use Twig\Node\TextNode;
use Twig\Token;
/**
* Marks a section of a template as untrusted code that must be evaluated in the sandbox mode.
*
* {% sandbox %}
* {% include 'user.html.twig' %}
* {% endsandbox %}
*
* @see https://twig.symfony.com/doc/api.html#sandbox-extension for details
*
* @internal
*/
final class SandboxTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
trigger_deprecation('twig/twig', '3.15', \sprintf('The "sandbox" tag is deprecated in "%s" at line %d.', $stream->getSourceContext()->getName(), $token->getLine()));
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
// in a sandbox tag, only include tags are allowed
if (!$body instanceof IncludeNode) {
foreach ($body as $node) {
if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) {
continue;
}
if (!$node instanceof IncludeNode) {
throw new SyntaxError('Only "include" tags are allowed within a "sandbox" section.', $node->getTemplateLine(), $stream->getSourceContext());
}
}
}
return new SandboxNode($body, $token->getLine());
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endsandbox');
}
public function getTag(): string
{
return 'sandbox';
}
}
+87
View File
@@ -0,0 +1,87 @@
<?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\Nodes;
use Twig\Node\SetNode;
use Twig\Token;
/**
* Defines a variable.
*
* {% set foo = 'foo' %}
* {% set foo = [1, 2] %}
* {% set foo = {'foo': 'bar'} %}
* {% set foo = 'foo' ~ 'bar' %}
* {% set foo, bar = 'foo', 'bar' %}
* {% set foo %}Some content{% endset %}
*
* @internal
*/
final class SetTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$names = $this->parseAssignmentExpression();
$capture = false;
if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) {
$values = $this->parseMultitargetExpression();
$stream->expect(Token::BLOCK_END_TYPE);
if (\count($names) !== \count($values)) {
throw new SyntaxError('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
} else {
$capture = true;
if (\count($names) > 1) {
throw new SyntaxError('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
$stream->expect(Token::BLOCK_END_TYPE);
$values = $this->parser->subparse([$this, 'decideBlockEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
}
return new SetNode($capture, $names, $values, $lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endset');
}
public function getTag(): string
{
return 'set';
}
private function parseMultitargetExpression(): Nodes
{
$targets = [];
while (true) {
$targets[] = $this->parser->parseExpression();
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
return new Nodes($targets);
}
}
+46
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\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\Node;
use Twig\Parser;
use Twig\Token;
/**
* Interface implemented by token parsers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TokenParserInterface
{
/**
* Sets the parser associated with this token parser.
*/
public function setParser(Parser $parser): void;
/**
* Parses a token and returns a node.
*
* @return Node
*
* @throws SyntaxError
*/
public function parse(Token $token);
/**
* Gets the tag name associated with this token parser.
*
* @return string
*/
public function getTag();
}
+89
View File
@@ -0,0 +1,89 @@
<?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
{
$enclosed = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '{');
$types = [];
$first = true;
while (!($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE))) {
if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma');
// trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE)) {
break;
}
}
$first = false;
$nameToken = $stream->expect(Token::NAME_TYPE);
if ($stream->nextIf(Token::OPERATOR_TYPE, '?:')) {
$isOptional = true;
} else {
$isOptional = null !== $stream->nextIf(Token::OPERATOR_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,
];
}
if ($enclosed) {
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
}
return $types;
}
public function getTag(): string
{
return 'types';
}
}
+75
View File
@@ -0,0 +1,75 @@
<?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\Expression\ConstantExpression;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Token;
/**
* Imports blocks defined in another template into the current template.
*
* {% extends "base.html" %}
*
* {% use "blocks.html" %}
*
* {% block title %}{% endblock %}
* {% block content %}{% endblock %}
*
* @see https://twig.symfony.com/doc/templates.html#horizontal-reuse for details.
*
* @internal
*/
final class UseTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$template = $this->parser->parseExpression();
$stream = $this->parser->getStream();
if (!$template instanceof ConstantExpression) {
throw new SyntaxError('The template references in a "use" statement must be a string.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
$targets = [];
if ($stream->nextIf('with')) {
while (true) {
$name = $stream->expect(Token::NAME_TYPE)->getValue();
$alias = $name;
if ($stream->nextIf('as')) {
$alias = $stream->expect(Token::NAME_TYPE)->getValue();
}
$targets[$name] = new ConstantExpression($alias, -1);
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
}
$stream->expect(Token::BLOCK_END_TYPE);
$this->parser->addTrait(new Nodes(['template' => $template, 'targets' => new Nodes($targets)]));
return new EmptyNode($token->getLine());
}
public function getTag(): string
{
return 'use';
}
}
+56
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\TokenParser;
use Twig\Node\Node;
use Twig\Node\WithNode;
use Twig\Token;
/**
* Creates a nested scope.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class WithTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$variables = null;
$only = false;
if (!$stream->test(Token::BLOCK_END_TYPE)) {
$variables = $this->parser->parseExpression();
$only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only');
}
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideWithEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
return new WithNode($body, $variables, $only, $token->getLine());
}
public function decideWithEnd(Token $token): bool
{
return $token->test('endwith');
}
public function getTag(): string
{
return 'with';
}
}
+131
View File
@@ -0,0 +1,131 @@
<?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;
use Twig\Error\SyntaxError;
/**
* Represents a token stream.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TokenStream
{
private $current = 0;
public function __construct(
private array $tokens,
private ?Source $source = null,
) {
if (null === $this->source) {
trigger_deprecation('twig/twig', '3.16', \sprintf('Not passing a "%s" object to "%s" constructor is deprecated.', Source::class, __CLASS__));
$this->source = new Source('', '');
}
}
public function __toString(): string
{
return implode("\n", $this->tokens);
}
/**
* @return void
*/
public function injectTokens(array $tokens)
{
$this->tokens = array_merge(\array_slice($this->tokens, 0, $this->current), $tokens, \array_slice($this->tokens, $this->current));
}
/**
* Sets the pointer to the next token and returns the old one.
*/
public function next(): Token
{
if (!isset($this->tokens[++$this->current])) {
throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current - 1]->getLine(), $this->source);
}
return $this->tokens[$this->current - 1];
}
/**
* Tests a token, sets the pointer to the next one and returns it or throws a syntax error.
*
* @return Token|null The next token if the condition is true, null otherwise
*/
public function nextIf($primary, $secondary = null)
{
return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null;
}
/**
* Tests a token and returns it or throws a syntax error.
*/
public function expect($type, $value = null, ?string $message = null): Token
{
$token = $this->tokens[$this->current];
if (!$token->test($type, $value)) {
$line = $token->getLine();
throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).',
$message ? $message.'. ' : '',
$token->toEnglish(),
$token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '',
Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''),
$line,
$this->source
);
}
$this->next();
return $token;
}
/**
* Looks at the next token.
*/
public function look(int $number = 1): Token
{
if (!isset($this->tokens[$this->current + $number])) {
throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current + $number - 1]->getLine(), $this->source);
}
return $this->tokens[$this->current + $number];
}
/**
* Tests the current token.
*/
public function test($primary, $secondary = null): bool
{
return $this->tokens[$this->current]->test($primary, $secondary);
}
/**
* Checks if end of stream was reached.
*/
public function isEOF(): bool
{
return $this->tokens[$this->current]->test(Token::EOF_TYPE);
}
public function getCurrent(): Token
{
return $this->tokens[$this->current];
}
public function getSourceContext(): Source
{
return $this->source;
}
}
+53
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;
}
+74
View File
@@ -0,0 +1,74 @@
<?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\Node\Expression\FilterExpression;
use Twig\Node\Node;
/**
* Represents a template filter.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see https://twig.symfony.com/doc/templates.html#filters
*/
final class TwigFilter extends AbstractTwigCallable
{
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
parent::__construct($name, $callable, $options);
$this->options = array_merge([
'is_safe' => null,
'is_safe_callback' => null,
'pre_escape' => null,
'preserves_safety' => null,
'node_class' => FilterExpression::class,
], $this->options);
}
public function getType(): string
{
return 'filter';
}
public function getSafe(Node $filterArgs): ?array
{
if (null !== $this->options['is_safe']) {
return $this->options['is_safe'];
}
if (null !== $this->options['is_safe_callback']) {
return $this->options['is_safe_callback']($filterArgs);
}
return [];
}
public function getPreservesSafety(): array
{
return $this->options['preserves_safety'] ?? [];
}
public function getPreEscape(): ?string
{
return $this->options['pre_escape'];
}
public function getMinimalNumberOfRequiredArguments(): int
{
return parent::getMinimalNumberOfRequiredArguments() + 1;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?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\Node\Expression\FunctionExpression;
use Twig\Node\Node;
/**
* Represents a template function.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see https://twig.symfony.com/doc/templates.html#functions
*/
final class TwigFunction extends AbstractTwigCallable
{
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
parent::__construct($name, $callable, $options);
$this->options = array_merge([
'is_safe' => null,
'is_safe_callback' => null,
'node_class' => FunctionExpression::class,
'parser_callable' => null,
], $this->options);
}
public function getType(): string
{
return 'function';
}
public function getParserCallable(): ?callable
{
return $this->options['parser_callable'];
}
public function getSafe(Node $functionArgs): ?array
{
if (null !== $this->options['is_safe']) {
return $this->options['is_safe'];
}
if (null !== $this->options['is_safe_callback']) {
return $this->options['is_safe_callback']($functionArgs);
}
return [];
}
}
+67
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;
use Twig\Node\Expression\TestExpression;
/**
* Represents a template test.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see https://twig.symfony.com/doc/templates.html#test-operator
*/
final class TwigTest extends AbstractTwigCallable
{
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
parent::__construct($name, $callable, $options);
$this->options = array_merge([
'node_class' => TestExpression::class,
'one_mandatory_argument' => false,
], $this->options);
}
public function getType(): string
{
return 'test';
}
public function needsCharset(): bool
{
return false;
}
public function needsEnvironment(): bool
{
return false;
}
public function needsContext(): bool
{
return false;
}
public function hasOneMandatoryArgument(): bool
{
return (bool) $this->options['one_mandatory_argument'];
}
public function getMinimalNumberOfRequiredArguments(): int
{
return parent::getMinimalNumberOfRequiredArguments() + 1;
}
}
+219
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];
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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\Environment;
use Twig\Error\SyntaxError;
use Twig\Source;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DeprecationCollector
{
public function __construct(
private Environment $twig,
) {
}
/**
* Returns deprecations for templates contained in a directory.
*
* @param string $dir A directory where templates are stored
* @param string $ext Limit the loaded templates by extension
*
* @return array An array of deprecations
*/
public function collectDir(string $dir, string $ext = '.twig'): array
{
$iterator = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY
), '{'.preg_quote($ext).'$}'
);
return $this->collect(new TemplateDirIterator($iterator));
}
/**
* Returns deprecations for passed templates.
*
* @param \Traversable $iterator An iterator of templates (where keys are template names and values the contents of the template)
*
* @return array An array of deprecations
*/
public function collect(\Traversable $iterator): array
{
$deprecations = [];
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
return false;
});
foreach ($iterator as $name => $contents) {
try {
$this->twig->parse($this->twig->tokenize(new Source($contents, $name)));
} catch (SyntaxError $e) {
// ignore templates containing syntax errors
}
}
restore_error_handler();
return $deprecations;
}
}
+95
View File
@@ -0,0 +1,95 @@
<?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;
}
/**
* @return callable
*/
public function getCallable()
{
return $this->callable;
}
public function getName(): string
{
return $this->name;
}
}
+36
View File
@@ -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\Util;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class TemplateDirIterator extends \IteratorIterator
{
/**
* @return string
*/
#[\ReturnTypeWillChange]
public function current()
{
return file_get_contents(parent::current());
}
/**
* @return string
*/
#[\ReturnTypeWillChange]
public function key()
{
return (string) parent::key();
}
}