1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\Extension {
13use Twig\ExpressionParser;
14use Twig\Node\Expression\Binary\AddBinary;
15use Twig\Node\Expression\Binary\AndBinary;
16use Twig\Node\Expression\Binary\BitwiseAndBinary;
17use Twig\Node\Expression\Binary\BitwiseOrBinary;
18use Twig\Node\Expression\Binary\BitwiseXorBinary;
19use Twig\Node\Expression\Binary\ConcatBinary;
20use Twig\Node\Expression\Binary\DivBinary;
21use Twig\Node\Expression\Binary\EndsWithBinary;
22use Twig\Node\Expression\Binary\EqualBinary;
23use Twig\Node\Expression\Binary\FloorDivBinary;
24use Twig\Node\Expression\Binary\GreaterBinary;
25use Twig\Node\Expression\Binary\GreaterEqualBinary;
26use Twig\Node\Expression\Binary\InBinary;
27use Twig\Node\Expression\Binary\LessBinary;
28use Twig\Node\Expression\Binary\LessEqualBinary;
29use Twig\Node\Expression\Binary\MatchesBinary;
30use Twig\Node\Expression\Binary\ModBinary;
31use Twig\Node\Expression\Binary\MulBinary;
32use Twig\Node\Expression\Binary\NotEqualBinary;
33use Twig\Node\Expression\Binary\NotInBinary;
34use Twig\Node\Expression\Binary\OrBinary;
35use Twig\Node\Expression\Binary\PowerBinary;
36use Twig\Node\Expression\Binary\RangeBinary;
37use Twig\Node\Expression\Binary\SpaceshipBinary;
38use Twig\Node\Expression\Binary\StartsWithBinary;
39use Twig\Node\Expression\Binary\SubBinary;
40use Twig\Node\Expression\Filter\DefaultFilter;
41use Twig\Node\Expression\NullCoalesceExpression;
42use Twig\Node\Expression\Test\ConstantTest;
43use Twig\Node\Expression\Test\DefinedTest;
44use Twig\Node\Expression\Test\DivisiblebyTest;
45use Twig\Node\Expression\Test\EvenTest;
46use Twig\Node\Expression\Test\NullTest;
47use Twig\Node\Expression\Test\OddTest;
48use Twig\Node\Expression\Test\SameasTest;
49use Twig\Node\Expression\Unary\NegUnary;
50use Twig\Node\Expression\Unary\NotUnary;
51use Twig\Node\Expression\Unary\PosUnary;
52use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
53use Twig\TokenParser\ApplyTokenParser;
54use Twig\TokenParser\BlockTokenParser;
55use Twig\TokenParser\DeprecatedTokenParser;
56use Twig\TokenParser\DoTokenParser;
57use Twig\TokenParser\EmbedTokenParser;
58use Twig\TokenParser\ExtendsTokenParser;
59use Twig\TokenParser\FilterTokenParser;
60use Twig\TokenParser\FlushTokenParser;
61use Twig\TokenParser\ForTokenParser;
62use Twig\TokenParser\FromTokenParser;
63use Twig\TokenParser\IfTokenParser;
64use Twig\TokenParser\ImportTokenParser;
65use Twig\TokenParser\IncludeTokenParser;
66use Twig\TokenParser\MacroTokenParser;
67use Twig\TokenParser\SetTokenParser;
68use Twig\TokenParser\SpacelessTokenParser;
69use Twig\TokenParser\UseTokenParser;
70use Twig\TokenParser\WithTokenParser;
71use Twig\TwigFilter;
72use Twig\TwigFunction;
73use Twig\TwigTest;
74
75final class CoreExtension extends AbstractExtension
76{
77    private $dateFormats = ['F j, Y H:i', '%d days'];
78    private $numberFormat = [0, '.', ','];
79    private $timezone = null;
80    private $escapers = [];
81
82    /**
83     * Defines a new escaper to be used via the escape filter.
84     *
85     * @param string   $strategy The strategy name that should be used as a strategy in the escape call
86     * @param callable $callable A valid PHP callable
87     *
88     * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
89     */
90    public function setEscaper($strategy, callable $callable)
91    {
92        @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::setEscaper" instead.', __METHOD__, EscaperExtension::class), \E_USER_DEPRECATED);
93
94        $this->escapers[$strategy] = $callable;
95    }
96
97    /**
98     * Gets all defined escapers.
99     *
100     * @return callable[] An array of escapers
101     *
102     * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
103     */
104    public function getEscapers(/* $triggerDeprecation = true */)
105    {
106        if (0 === \func_num_args() || \func_get_arg(0)) {
107            @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::getEscapers" instead.', __METHOD__, EscaperExtension::class), \E_USER_DEPRECATED);
108        }
109
110        return $this->escapers;
111    }
112
113    /**
114     * Sets the default format to be used by the date filter.
115     *
116     * @param string $format             The default date format string
117     * @param string $dateIntervalFormat The default date interval format string
118     */
119    public function setDateFormat($format = null, $dateIntervalFormat = null)
120    {
121        if (null !== $format) {
122            $this->dateFormats[0] = $format;
123        }
124
125        if (null !== $dateIntervalFormat) {
126            $this->dateFormats[1] = $dateIntervalFormat;
127        }
128    }
129
130    /**
131     * Gets the default format to be used by the date filter.
132     *
133     * @return array The default date format string and the default date interval format string
134     */
135    public function getDateFormat()
136    {
137        return $this->dateFormats;
138    }
139
140    /**
141     * Sets the default timezone to be used by the date filter.
142     *
143     * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object
144     */
145    public function setTimezone($timezone)
146    {
147        $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone);
148    }
149
150    /**
151     * Gets the default timezone to be used by the date filter.
152     *
153     * @return \DateTimeZone The default timezone currently in use
154     */
155    public function getTimezone()
156    {
157        if (null === $this->timezone) {
158            $this->timezone = new \DateTimeZone(date_default_timezone_get());
159        }
160
161        return $this->timezone;
162    }
163
164    /**
165     * Sets the default format to be used by the number_format filter.
166     *
167     * @param int    $decimal      the number of decimal places to use
168     * @param string $decimalPoint the character(s) to use for the decimal point
169     * @param string $thousandSep  the character(s) to use for the thousands separator
170     */
171    public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
172    {
173        $this->numberFormat = [$decimal, $decimalPoint, $thousandSep];
174    }
175
176    /**
177     * Get the default format used by the number_format filter.
178     *
179     * @return array The arguments for number_format()
180     */
181    public function getNumberFormat()
182    {
183        return $this->numberFormat;
184    }
185
186    public function getTokenParsers()
187    {
188        return [
189            new ApplyTokenParser(),
190            new ForTokenParser(),
191            new IfTokenParser(),
192            new ExtendsTokenParser(),
193            new IncludeTokenParser(),
194            new BlockTokenParser(),
195            new UseTokenParser(),
196            new FilterTokenParser(),
197            new MacroTokenParser(),
198            new ImportTokenParser(),
199            new FromTokenParser(),
200            new SetTokenParser(),
201            new SpacelessTokenParser(),
202            new FlushTokenParser(),
203            new DoTokenParser(),
204            new EmbedTokenParser(),
205            new WithTokenParser(),
206            new DeprecatedTokenParser(),
207        ];
208    }
209
210    public function getFilters()
211    {
212        return [
213            // formatting filters
214            new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]),
215            new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]),
216            new TwigFilter('format', 'twig_sprintf'),
217            new TwigFilter('replace', 'twig_replace_filter'),
218            new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]),
219            new TwigFilter('abs', 'abs'),
220            new TwigFilter('round', 'twig_round'),
221
222            // encoding
223            new TwigFilter('url_encode', 'twig_urlencode_filter'),
224            new TwigFilter('json_encode', 'json_encode'),
225            new TwigFilter('convert_encoding', 'twig_convert_encoding'),
226
227            // string filters
228            new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]),
229            new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]),
230            new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]),
231            new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]),
232            new TwigFilter('striptags', 'twig_striptags'),
233            new TwigFilter('trim', 'twig_trim_filter'),
234            new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]),
235            new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]),
236
237            // array helpers
238            new TwigFilter('join', 'twig_join_filter'),
239            new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]),
240            new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]),
241            new TwigFilter('merge', 'twig_array_merge'),
242            new TwigFilter('batch', 'twig_array_batch'),
243            new TwigFilter('column', 'twig_array_column'),
244            new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]),
245            new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]),
246            new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]),
247
248            // string/array filters
249            new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]),
250            new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]),
251            new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]),
252            new TwigFilter('first', 'twig_first', ['needs_environment' => true]),
253            new TwigFilter('last', 'twig_last', ['needs_environment' => true]),
254
255            // iteration and runtime
256            new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]),
257            new TwigFilter('keys', 'twig_get_array_keys_filter'),
258        ];
259    }
260
261    public function getFunctions()
262    {
263        return [
264            new TwigFunction('max', 'max'),
265            new TwigFunction('min', 'min'),
266            new TwigFunction('range', 'range'),
267            new TwigFunction('constant', 'twig_constant'),
268            new TwigFunction('cycle', 'twig_cycle'),
269            new TwigFunction('random', 'twig_random', ['needs_environment' => true]),
270            new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]),
271            new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
272            new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]),
273        ];
274    }
275
276    public function getTests()
277    {
278        return [
279            new TwigTest('even', null, ['node_class' => EvenTest::class]),
280            new TwigTest('odd', null, ['node_class' => OddTest::class]),
281            new TwigTest('defined', null, ['node_class' => DefinedTest::class]),
282            new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true]),
283            new TwigTest('none', null, ['node_class' => NullTest::class]),
284            new TwigTest('null', null, ['node_class' => NullTest::class]),
285            new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]),
286            new TwigTest('constant', null, ['node_class' => ConstantTest::class]),
287            new TwigTest('empty', 'twig_test_empty'),
288            new TwigTest('iterable', 'twig_test_iterable'),
289        ];
290    }
291
292    public function getNodeVisitors()
293    {
294        return [new MacroAutoImportNodeVisitor()];
295    }
296
297    public function getOperators()
298    {
299        return [
300            [
301                'not' => ['precedence' => 50, 'class' => NotUnary::class],
302                '-' => ['precedence' => 500, 'class' => NegUnary::class],
303                '+' => ['precedence' => 500, 'class' => PosUnary::class],
304            ],
305            [
306                'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
307                'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
308                'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
309                'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
310                'b-and' => ['precedence' => 18, 'class' => BitwiseAndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
311                '==' => ['precedence' => 20, 'class' => EqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
312                '!=' => ['precedence' => 20, 'class' => NotEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
313                '<=>' => ['precedence' => 20, 'class' => SpaceshipBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
314                '<' => ['precedence' => 20, 'class' => LessBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
315                '>' => ['precedence' => 20, 'class' => GreaterBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
316                '>=' => ['precedence' => 20, 'class' => GreaterEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
317                '<=' => ['precedence' => 20, 'class' => LessEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
318                'not in' => ['precedence' => 20, 'class' => NotInBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
319                'in' => ['precedence' => 20, 'class' => InBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
320                'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
321                'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
322                'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
323                '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
324                '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
325                '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
326                '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
327                '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
328                '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
329                '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
330                '%' => ['precedence' => 60, 'class' => ModBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
331                'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
332                'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
333                '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
334                '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
335            ],
336        ];
337    }
338}
339
340class_alias('Twig\Extension\CoreExtension', 'Twig_Extension_Core');
341}
342
343namespace {
344    use Twig\Environment;
345    use Twig\Error\LoaderError;
346    use Twig\Error\RuntimeError;
347    use Twig\Extension\CoreExtension;
348    use Twig\Extension\SandboxExtension;
349    use Twig\Markup;
350    use Twig\Source;
351    use Twig\Template;
352    use Twig\TemplateWrapper;
353
354/**
355 * Cycles over a value.
356 *
357 * @param \ArrayAccess|array $values
358 * @param int                $position The cycle position
359 *
360 * @return string The next value in the cycle
361 */
362function twig_cycle($values, $position)
363{
364    if (!\is_array($values) && !$values instanceof \ArrayAccess) {
365        return $values;
366    }
367
368    return $values[$position % \count($values)];
369}
370
371/**
372 * Returns a random value depending on the supplied parameter type:
373 * - a random item from a \Traversable or array
374 * - a random character from a string
375 * - a random integer between 0 and the integer parameter.
376 *
377 * @param \Traversable|array|int|float|string $values The values to pick a random item from
378 * @param int|null                            $max    Maximum value used when $values is an int
379 *
380 * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
381 *
382 * @return mixed A random value from the given sequence
383 */
384function twig_random(Environment $env, $values = null, $max = null)
385{
386    if (null === $values) {
387        return null === $max ? mt_rand() : mt_rand(0, (int) $max);
388    }
389
390    if (\is_int($values) || \is_float($values)) {
391        if (null === $max) {
392            if ($values < 0) {
393                $max = 0;
394                $min = $values;
395            } else {
396                $max = $values;
397                $min = 0;
398            }
399        } else {
400            $min = $values;
401            $max = $max;
402        }
403
404        return mt_rand((int) $min, (int) $max);
405    }
406
407    if (\is_string($values)) {
408        if ('' === $values) {
409            return '';
410        }
411
412        $charset = $env->getCharset();
413
414        if ('UTF-8' !== $charset) {
415            $values = twig_convert_encoding($values, 'UTF-8', $charset);
416        }
417
418        // unicode version of str_split()
419        // split at all positions, but not after the start and not before the end
420        $values = preg_split('/(?<!^)(?!$)/u', $values);
421
422        if ('UTF-8' !== $charset) {
423            foreach ($values as $i => $value) {
424                $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
425            }
426        }
427    }
428
429    if (!twig_test_iterable($values)) {
430        return $values;
431    }
432
433    $values = twig_to_array($values);
434
435    if (0 === \count($values)) {
436        throw new RuntimeError('The random function cannot pick from an empty array.');
437    }
438
439    return $values[array_rand($values, 1)];
440}
441
442/**
443 * Converts a date to the given format.
444 *
445 *   {{ post.published_at|date("m/d/Y") }}
446 *
447 * @param \DateTimeInterface|\DateInterval|string $date     A date
448 * @param string|null                             $format   The target format, null to use the default
449 * @param \DateTimeZone|string|false|null         $timezone The target timezone, null to use the default, false to leave unchanged
450 *
451 * @return string The formatted date
452 */
453function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
454{
455    if (null === $format) {
456        $formats = $env->getExtension(CoreExtension::class)->getDateFormat();
457        $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
458    }
459
460    if ($date instanceof \DateInterval) {
461        return $date->format($format);
462    }
463
464    return twig_date_converter($env, $date, $timezone)->format($format);
465}
466
467/**
468 * Returns a new date object modified.
469 *
470 *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
471 *
472 * @param \DateTimeInterface|string $date     A date
473 * @param string                    $modifier A modifier string
474 *
475 * @return \DateTimeInterface
476 */
477function twig_date_modify_filter(Environment $env, $date, $modifier)
478{
479    $date = twig_date_converter($env, $date, false);
480
481    return $date->modify($modifier);
482}
483
484/**
485 * Returns a formatted string.
486 *
487 * @param string|null $format
488 * @param ...$values
489 *
490 * @return string
491 */
492function twig_sprintf($format, ...$values)
493{
494    return sprintf($format ?? '', ...$values);
495}
496
497/**
498 * Converts an input to a \DateTime instance.
499 *
500 *    {% if date(user.created_at) < date('+2days') %}
501 *      {# do something #}
502 *    {% endif %}
503 *
504 * @param \DateTimeInterface|string|null  $date     A date or null to use the current time
505 * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
506 *
507 * @return \DateTimeInterface
508 */
509function twig_date_converter(Environment $env, $date = null, $timezone = null)
510{
511    // determine the timezone
512    if (false !== $timezone) {
513        if (null === $timezone) {
514            $timezone = $env->getExtension(CoreExtension::class)->getTimezone();
515        } elseif (!$timezone instanceof \DateTimeZone) {
516            $timezone = new \DateTimeZone($timezone);
517        }
518    }
519
520    // immutable dates
521    if ($date instanceof \DateTimeImmutable) {
522        return false !== $timezone ? $date->setTimezone($timezone) : $date;
523    }
524
525    if ($date instanceof \DateTimeInterface) {
526        $date = clone $date;
527        if (false !== $timezone) {
528            $date->setTimezone($timezone);
529        }
530
531        return $date;
532    }
533
534    if (null === $date || 'now' === $date) {
535        if (null === $date) {
536            $date = 'now';
537        }
538
539        return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone());
540    }
541
542    $asString = (string) $date;
543    if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
544        $date = new \DateTime('@'.$date);
545    } else {
546        $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone());
547    }
548
549    if (false !== $timezone) {
550        $date->setTimezone($timezone);
551    }
552
553    return $date;
554}
555
556/**
557 * Replaces strings within a string.
558 *
559 * @param string|null        $str  String to replace in
560 * @param array|\Traversable $from Replace values
561 *
562 * @return string
563 */
564function twig_replace_filter($str, $from)
565{
566    if (!twig_test_iterable($from)) {
567        throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from)));
568    }
569
570    return strtr($str ?? '', twig_to_array($from));
571}
572
573/**
574 * Rounds a number.
575 *
576 * @param int|float|string|null $value     The value to round
577 * @param int|float             $precision The rounding precision
578 * @param string                $method    The method to use for rounding
579 *
580 * @return int|float The rounded number
581 */
582function twig_round($value, $precision = 0, $method = 'common')
583{
584    $value = (float) $value;
585
586    if ('common' === $method) {
587        return round($value, $precision);
588    }
589
590    if ('ceil' !== $method && 'floor' !== $method) {
591        throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
592    }
593
594    return $method($value * 10 ** $precision) / 10 ** $precision;
595}
596
597/**
598 * Number format filter.
599 *
600 * All of the formatting options can be left null, in that case the defaults will
601 * be used. Supplying any of the parameters will override the defaults set in the
602 * environment object.
603 *
604 * @param mixed  $number       A float/int/string of the number to format
605 * @param int    $decimal      the number of decimal points to display
606 * @param string $decimalPoint the character(s) to use for the decimal point
607 * @param string $thousandSep  the character(s) to use for the thousands separator
608 *
609 * @return string The formatted number
610 */
611function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
612{
613    $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat();
614    if (null === $decimal) {
615        $decimal = $defaults[0];
616    }
617
618    if (null === $decimalPoint) {
619        $decimalPoint = $defaults[1];
620    }
621
622    if (null === $thousandSep) {
623        $thousandSep = $defaults[2];
624    }
625
626    return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
627}
628
629/**
630 * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
631 *
632 * @param string|array|null $url A URL or an array of query parameters
633 *
634 * @return string The URL encoded value
635 */
636function twig_urlencode_filter($url)
637{
638    if (\is_array($url)) {
639        return http_build_query($url, '', '&', \PHP_QUERY_RFC3986);
640    }
641
642    return rawurlencode($url ?? '');
643}
644
645/**
646 * Merges an array with another one.
647 *
648 *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
649 *
650 *  {% set items = items|merge({ 'peugeot': 'car' }) %}
651 *
652 *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
653 *
654 * @param array|\Traversable $arr1 An array
655 * @param array|\Traversable $arr2 An array
656 *
657 * @return array The merged array
658 */
659function twig_array_merge($arr1, $arr2)
660{
661    if (!twig_test_iterable($arr1)) {
662        throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1)));
663    }
664
665    if (!twig_test_iterable($arr2)) {
666        throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2)));
667    }
668
669    return array_merge(twig_to_array($arr1), twig_to_array($arr2));
670}
671
672/**
673 * Slices a variable.
674 *
675 * @param mixed $item         A variable
676 * @param int   $start        Start of the slice
677 * @param int   $length       Size of the slice
678 * @param bool  $preserveKeys Whether to preserve key or not (when the input is an array)
679 *
680 * @return mixed The sliced variable
681 */
682function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
683{
684    if ($item instanceof \Traversable) {
685        while ($item instanceof \IteratorAggregate) {
686            $item = $item->getIterator();
687        }
688
689        if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
690            try {
691                return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys);
692            } catch (\OutOfBoundsException $e) {
693                return [];
694            }
695        }
696
697        $item = iterator_to_array($item, $preserveKeys);
698    }
699
700    if (\is_array($item)) {
701        return \array_slice($item, $start, $length, $preserveKeys);
702    }
703
704    return (string) mb_substr((string) $item, $start, $length, $env->getCharset());
705}
706
707/**
708 * Returns the first element of the item.
709 *
710 * @param mixed $item A variable
711 *
712 * @return mixed The first element of the item
713 */
714function twig_first(Environment $env, $item)
715{
716    $elements = twig_slice($env, $item, 0, 1, false);
717
718    return \is_string($elements) ? $elements : current($elements);
719}
720
721/**
722 * Returns the last element of the item.
723 *
724 * @param mixed $item A variable
725 *
726 * @return mixed The last element of the item
727 */
728function twig_last(Environment $env, $item)
729{
730    $elements = twig_slice($env, $item, -1, 1, false);
731
732    return \is_string($elements) ? $elements : current($elements);
733}
734
735/**
736 * Joins the values to a string.
737 *
738 * The separators between elements are empty strings per default, you can define them with the optional parameters.
739 *
740 *  {{ [1, 2, 3]|join(', ', ' and ') }}
741 *  {# returns 1, 2 and 3 #}
742 *
743 *  {{ [1, 2, 3]|join('|') }}
744 *  {# returns 1|2|3 #}
745 *
746 *  {{ [1, 2, 3]|join }}
747 *  {# returns 123 #}
748 *
749 * @param array       $value An array
750 * @param string      $glue  The separator
751 * @param string|null $and   The separator for the last pair
752 *
753 * @return string The concatenated string
754 */
755function twig_join_filter($value, $glue = '', $and = null)
756{
757    if (!twig_test_iterable($value)) {
758        $value = (array) $value;
759    }
760
761    $value = twig_to_array($value, false);
762
763    if (0 === \count($value)) {
764        return '';
765    }
766
767    if (null === $and || $and === $glue) {
768        return implode($glue, $value);
769    }
770
771    if (1 === \count($value)) {
772        return $value[0];
773    }
774
775    return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
776}
777
778/**
779 * Splits the string into an array.
780 *
781 *  {{ "one,two,three"|split(',') }}
782 *  {# returns [one, two, three] #}
783 *
784 *  {{ "one,two,three,four,five"|split(',', 3) }}
785 *  {# returns [one, two, "three,four,five"] #}
786 *
787 *  {{ "123"|split('') }}
788 *  {# returns [1, 2, 3] #}
789 *
790 *  {{ "aabbcc"|split('', 2) }}
791 *  {# returns [aa, bb, cc] #}
792 *
793 * @param string|null $value     A string
794 * @param string      $delimiter The delimiter
795 * @param int         $limit     The limit
796 *
797 * @return array The split string as an array
798 */
799function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
800{
801    $value = $value ?? '';
802
803    if (\strlen($delimiter) > 0) {
804        return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
805    }
806
807    if ($limit <= 1) {
808        return preg_split('/(?<!^)(?!$)/u', $value);
809    }
810
811    $length = mb_strlen($value, $env->getCharset());
812    if ($length < $limit) {
813        return [$value];
814    }
815
816    $r = [];
817    for ($i = 0; $i < $length; $i += $limit) {
818        $r[] = mb_substr($value, $i, $limit, $env->getCharset());
819    }
820
821    return $r;
822}
823
824// The '_default' filter is used internally to avoid using the ternary operator
825// which costs a lot for big contexts (before PHP 5.4). So, on average,
826// a function call is cheaper.
827/**
828 * @internal
829 */
830function _twig_default_filter($value, $default = '')
831{
832    if (twig_test_empty($value)) {
833        return $default;
834    }
835
836    return $value;
837}
838
839/**
840 * Returns the keys for the given array.
841 *
842 * It is useful when you want to iterate over the keys of an array:
843 *
844 *  {% for key in array|keys %}
845 *      {# ... #}
846 *  {% endfor %}
847 *
848 * @param array $array An array
849 *
850 * @return array The keys
851 */
852function twig_get_array_keys_filter($array)
853{
854    if ($array instanceof \Traversable) {
855        while ($array instanceof \IteratorAggregate) {
856            $array = $array->getIterator();
857        }
858
859        if ($array instanceof \Iterator) {
860            $keys = [];
861            $array->rewind();
862            while ($array->valid()) {
863                $keys[] = $array->key();
864                $array->next();
865            }
866
867            return $keys;
868        }
869
870        $keys = [];
871        foreach ($array as $key => $item) {
872            $keys[] = $key;
873        }
874
875        return $keys;
876    }
877
878    if (!\is_array($array)) {
879        return [];
880    }
881
882    return array_keys($array);
883}
884
885/**
886 * Reverses a variable.
887 *
888 * @param array|\Traversable|string|null $item         An array, a \Traversable instance, or a string
889 * @param bool                           $preserveKeys Whether to preserve key or not
890 *
891 * @return mixed The reversed input
892 */
893function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
894{
895    if ($item instanceof \Traversable) {
896        return array_reverse(iterator_to_array($item), $preserveKeys);
897    }
898
899    if (\is_array($item)) {
900        return array_reverse($item, $preserveKeys);
901    }
902
903    $string = (string) $item;
904
905    $charset = $env->getCharset();
906
907    if ('UTF-8' !== $charset) {
908        $string = twig_convert_encoding($string, 'UTF-8', $charset);
909    }
910
911    preg_match_all('/./us', $string, $matches);
912
913    $string = implode('', array_reverse($matches[0]));
914
915    if ('UTF-8' !== $charset) {
916        $string = twig_convert_encoding($string, $charset, 'UTF-8');
917    }
918
919    return $string;
920}
921
922/**
923 * Sorts an array.
924 *
925 * @param array|\Traversable $array
926 *
927 * @return array
928 */
929function twig_sort_filter(Environment $env, $array, $arrow = null)
930{
931    if ($array instanceof \Traversable) {
932        $array = iterator_to_array($array);
933    } elseif (!\is_array($array)) {
934        throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
935    }
936
937    if (null !== $arrow) {
938        twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter');
939
940        uasort($array, $arrow);
941    } else {
942        asort($array);
943    }
944
945    return $array;
946}
947
948/**
949 * @internal
950 */
951function twig_in_filter($value, $compare)
952{
953    if ($value instanceof Markup) {
954        $value = (string) $value;
955    }
956    if ($compare instanceof Markup) {
957        $compare = (string) $compare;
958    }
959
960    if (\is_array($compare)) {
961        return \in_array($value, $compare, \is_object($value) || \is_resource($value));
962    } elseif (\is_string($compare) && (\is_string($value) || \is_int($value) || \is_float($value))) {
963        return '' === $value || false !== strpos($compare, (string) $value);
964    } elseif ($compare instanceof \Traversable) {
965        if (\is_object($value) || \is_resource($value)) {
966            foreach ($compare as $item) {
967                if ($item === $value) {
968                    return true;
969                }
970            }
971        } else {
972            foreach ($compare as $item) {
973                if ($item == $value) {
974                    return true;
975                }
976            }
977        }
978
979        return false;
980    }
981
982    return false;
983}
984
985/**
986 * Returns a trimmed string.
987 *
988 * @param string|null $string
989 * @param string|null $characterMask
990 * @param string      $side
991 *
992 * @return string
993 *
994 * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
995 */
996function twig_trim_filter($string, $characterMask = null, $side = 'both')
997{
998    if (null === $characterMask) {
999        $characterMask = " \t\n\r\0\x0B";
1000    }
1001
1002    switch ($side) {
1003        case 'both':
1004            return trim($string ?? '', $characterMask);
1005        case 'left':
1006            return ltrim($string ?? '', $characterMask);
1007        case 'right':
1008            return rtrim($string ?? '', $characterMask);
1009        default:
1010            throw new RuntimeError('Trimming side must be "left", "right" or "both".');
1011    }
1012}
1013
1014/**
1015 * Inserts HTML line breaks before all newlines in a string.
1016 *
1017 * @param string|null $string
1018 *
1019 * @return string
1020 */
1021function twig_nl2br($string)
1022{
1023    return nl2br($string ?? '');
1024}
1025
1026/**
1027 * Removes whitespaces between HTML tags.
1028 *
1029 * @param string|null $string
1030 *
1031 * @return string
1032 */
1033function twig_spaceless($content)
1034{
1035    return trim(preg_replace('/>\s+</', '><', $content ?? ''));
1036}
1037
1038/**
1039 * @param string|null $string
1040 * @param string      $to
1041 * @param string      $from
1042 *
1043 * @return string
1044 */
1045function twig_convert_encoding($string, $to, $from)
1046{
1047    if (!\function_exists('iconv')) {
1048        throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
1049    }
1050
1051    return iconv($from, $to, $string ?? '');
1052}
1053
1054/**
1055 * Returns the length of a variable.
1056 *
1057 * @param mixed $thing A variable
1058 *
1059 * @return int The length of the value
1060 */
1061function twig_length_filter(Environment $env, $thing)
1062{
1063    if (null === $thing) {
1064        return 0;
1065    }
1066
1067    if (is_scalar($thing)) {
1068        return mb_strlen($thing, $env->getCharset());
1069    }
1070
1071    if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
1072        return \count($thing);
1073    }
1074
1075    if ($thing instanceof \Traversable) {
1076        return iterator_count($thing);
1077    }
1078
1079    if (method_exists($thing, '__toString') && !$thing instanceof \Countable) {
1080        return mb_strlen((string) $thing, $env->getCharset());
1081    }
1082
1083    return 1;
1084}
1085
1086/**
1087 * Converts a string to uppercase.
1088 *
1089 * @param string|null $string A string
1090 *
1091 * @return string The uppercased string
1092 */
1093function twig_upper_filter(Environment $env, $string)
1094{
1095    return mb_strtoupper($string ?? '', $env->getCharset());
1096}
1097
1098/**
1099 * Converts a string to lowercase.
1100 *
1101 * @param string|null $string A string
1102 *
1103 * @return string The lowercased string
1104 */
1105function twig_lower_filter(Environment $env, $string)
1106{
1107    return mb_strtolower($string ?? '', $env->getCharset());
1108}
1109
1110/**
1111 * Strips HTML and PHP tags from a string.
1112 *
1113 * @param string|null $string
1114 * @param string[]|string|null $string
1115 *
1116 * @return string
1117 */
1118function twig_striptags($string, $allowable_tags = null)
1119{
1120    return strip_tags($string ?? '', $allowable_tags);
1121}
1122
1123/**
1124 * Returns a titlecased string.
1125 *
1126 * @param string|null $string A string
1127 *
1128 * @return string The titlecased string
1129 */
1130function twig_title_string_filter(Environment $env, $string)
1131{
1132    if (null !== $charset = $env->getCharset()) {
1133        return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset);
1134    }
1135
1136    return ucwords(strtolower($string ?? ''));
1137}
1138
1139/**
1140 * Returns a capitalized string.
1141 *
1142 * @param string|null $string A string
1143 *
1144 * @return string The capitalized string
1145 */
1146function twig_capitalize_string_filter(Environment $env, $string)
1147{
1148    $charset = $env->getCharset();
1149
1150    return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset);
1151}
1152
1153/**
1154 * @internal
1155 */
1156function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
1157{
1158    if (!method_exists($template, $method)) {
1159        $parent = $template;
1160        while ($parent = $parent->getParent($context)) {
1161            if (method_exists($parent, $method)) {
1162                return $parent->$method(...$args);
1163            }
1164        }
1165
1166        throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source);
1167    }
1168
1169    return $template->$method(...$args);
1170}
1171
1172/**
1173 * @internal
1174 */
1175function twig_ensure_traversable($seq)
1176{
1177    if ($seq instanceof \Traversable || \is_array($seq)) {
1178        return $seq;
1179    }
1180
1181    return [];
1182}
1183
1184/**
1185 * @internal
1186 */
1187function twig_to_array($seq, $preserveKeys = true)
1188{
1189    if ($seq instanceof \Traversable) {
1190        return iterator_to_array($seq, $preserveKeys);
1191    }
1192
1193    if (!\is_array($seq)) {
1194        return $seq;
1195    }
1196
1197    return $preserveKeys ? $seq : array_values($seq);
1198}
1199
1200/**
1201 * Checks if a variable is empty.
1202 *
1203 *    {# evaluates to true if the foo variable is null, false, or the empty string #}
1204 *    {% if foo is empty %}
1205 *        {# ... #}
1206 *    {% endif %}
1207 *
1208 * @param mixed $value A variable
1209 *
1210 * @return bool true if the value is empty, false otherwise
1211 */
1212function twig_test_empty($value)
1213{
1214    if ($value instanceof \Countable) {
1215        return 0 === \count($value);
1216    }
1217
1218    if ($value instanceof \Traversable) {
1219        return !iterator_count($value);
1220    }
1221
1222    if (\is_object($value) && method_exists($value, '__toString')) {
1223        return '' === (string) $value;
1224    }
1225
1226    return '' === $value || false === $value || null === $value || [] === $value;
1227}
1228
1229/**
1230 * Checks if a variable is traversable.
1231 *
1232 *    {# evaluates to true if the foo variable is an array or a traversable object #}
1233 *    {% if foo is iterable %}
1234 *        {# ... #}
1235 *    {% endif %}
1236 *
1237 * @param mixed $value A variable
1238 *
1239 * @return bool true if the value is traversable
1240 */
1241function twig_test_iterable($value)
1242{
1243    return $value instanceof \Traversable || \is_array($value);
1244}
1245
1246/**
1247 * Renders a template.
1248 *
1249 * @param array        $context
1250 * @param string|array $template      The template to render or an array of templates to try consecutively
1251 * @param array        $variables     The variables to pass to the template
1252 * @param bool         $withContext
1253 * @param bool         $ignoreMissing Whether to ignore missing templates or not
1254 * @param bool         $sandboxed     Whether to sandbox the template or not
1255 *
1256 * @return string The rendered template
1257 */
1258function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
1259{
1260    $alreadySandboxed = false;
1261    $sandbox = null;
1262    if ($withContext) {
1263        $variables = array_merge($context, $variables);
1264    }
1265
1266    if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
1267        $sandbox = $env->getExtension(SandboxExtension::class);
1268        if (!$alreadySandboxed = $sandbox->isSandboxed()) {
1269            $sandbox->enableSandbox();
1270        }
1271
1272        foreach ((\is_array($template) ? $template : [$template]) as $name) {
1273            // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security
1274            if ($name instanceof TemplateWrapper || $name instanceof Template) {
1275                $name->unwrap()->checkSecurity();
1276            }
1277        }
1278    }
1279
1280    try {
1281        $loaded = null;
1282        try {
1283            $loaded = $env->resolveTemplate($template);
1284        } catch (LoaderError $e) {
1285            if (!$ignoreMissing) {
1286                throw $e;
1287            }
1288        }
1289
1290        return $loaded ? $loaded->render($variables) : '';
1291    } finally {
1292        if ($isSandboxed && !$alreadySandboxed) {
1293            $sandbox->disableSandbox();
1294        }
1295    }
1296}
1297
1298/**
1299 * Returns a template content without rendering it.
1300 *
1301 * @param string $name          The template name
1302 * @param bool   $ignoreMissing Whether to ignore missing templates or not
1303 *
1304 * @return string The template source
1305 */
1306function twig_source(Environment $env, $name, $ignoreMissing = false)
1307{
1308    $loader = $env->getLoader();
1309    try {
1310        return $loader->getSourceContext($name)->getCode();
1311    } catch (LoaderError $e) {
1312        if (!$ignoreMissing) {
1313            throw $e;
1314        }
1315    }
1316}
1317
1318/**
1319 * Provides the ability to get constants from instances as well as class/global constants.
1320 *
1321 * @param string      $constant The name of the constant
1322 * @param object|null $object   The object to get the constant from
1323 *
1324 * @return string
1325 */
1326function twig_constant($constant, $object = null)
1327{
1328    if (null !== $object) {
1329        $constant = \get_class($object).'::'.$constant;
1330    }
1331
1332    return \constant($constant);
1333}
1334
1335/**
1336 * Checks if a constant exists.
1337 *
1338 * @param string      $constant The name of the constant
1339 * @param object|null $object   The object to get the constant from
1340 *
1341 * @return bool
1342 */
1343function twig_constant_is_defined($constant, $object = null)
1344{
1345    if (null !== $object) {
1346        $constant = \get_class($object).'::'.$constant;
1347    }
1348
1349    return \defined($constant);
1350}
1351
1352/**
1353 * Batches item.
1354 *
1355 * @param array $items An array of items
1356 * @param int   $size  The size of the batch
1357 * @param mixed $fill  A value used to fill missing items
1358 *
1359 * @return array
1360 */
1361function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
1362{
1363    if (!twig_test_iterable($items)) {
1364        throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items)));
1365    }
1366
1367    $size = ceil($size);
1368
1369    $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys);
1370
1371    if (null !== $fill && $result) {
1372        $last = \count($result) - 1;
1373        if ($fillCount = $size - \count($result[$last])) {
1374            for ($i = 0; $i < $fillCount; ++$i) {
1375                $result[$last][] = $fill;
1376            }
1377        }
1378    }
1379
1380    return $result;
1381}
1382
1383/**
1384 * Returns the attribute value for a given array/object.
1385 *
1386 * @param mixed  $object            The object or array from where to get the item
1387 * @param mixed  $item              The item to get from the array or object
1388 * @param array  $arguments         An array of arguments to pass if the item is an object method
1389 * @param string $type              The type of attribute (@see \Twig\Template constants)
1390 * @param bool   $isDefinedTest     Whether this is only a defined check
1391 * @param bool   $ignoreStrictCheck Whether to ignore the strict attribute check or not
1392 * @param int    $lineno            The template line where the attribute was called
1393 *
1394 * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true
1395 *
1396 * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false
1397 *
1398 * @internal
1399 */
1400function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
1401{
1402    // array
1403    if (/* Template::METHOD_CALL */ 'method' !== $type) {
1404        $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
1405
1406        if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object)))
1407            || ($object instanceof ArrayAccess && isset($object[$arrayItem]))
1408        ) {
1409            if ($isDefinedTest) {
1410                return true;
1411            }
1412
1413            return $object[$arrayItem];
1414        }
1415
1416        if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) {
1417            if ($isDefinedTest) {
1418                return false;
1419            }
1420
1421            if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1422                return;
1423            }
1424
1425            if ($object instanceof ArrayAccess) {
1426                $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object));
1427            } elseif (\is_object($object)) {
1428                $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object));
1429            } elseif (\is_array($object)) {
1430                if (empty($object)) {
1431                    $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem);
1432                } else {
1433                    $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
1434                }
1435            } elseif (/* Template::ARRAY_CALL */ 'array' === $type) {
1436                if (null === $object) {
1437                    $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item);
1438                } else {
1439                    $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
1440                }
1441            } elseif (null === $object) {
1442                $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item);
1443            } else {
1444                $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
1445            }
1446
1447            throw new RuntimeError($message, $lineno, $source);
1448        }
1449    }
1450
1451    if (!\is_object($object)) {
1452        if ($isDefinedTest) {
1453            return false;
1454        }
1455
1456        if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1457            return;
1458        }
1459
1460        if (null === $object) {
1461            $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
1462        } elseif (\is_array($object)) {
1463            $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item);
1464        } else {
1465            $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
1466        }
1467
1468        throw new RuntimeError($message, $lineno, $source);
1469    }
1470
1471    if ($object instanceof Template) {
1472        throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source);
1473    }
1474
1475    // object property
1476    if (/* Template::METHOD_CALL */ 'method' !== $type) {
1477        if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) {
1478            if ($isDefinedTest) {
1479                return true;
1480            }
1481
1482            if ($sandboxed) {
1483                $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
1484            }
1485
1486            return $object->$item;
1487        }
1488    }
1489
1490    static $cache = [];
1491
1492    $class = \get_class($object);
1493
1494    // object method
1495    // precedence: getXxx() > isXxx() > hasXxx()
1496    if (!isset($cache[$class])) {
1497        $methods = get_class_methods($object);
1498        sort($methods);
1499        $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods);
1500        $classCache = [];
1501        foreach ($methods as $i => $method) {
1502            $classCache[$method] = $method;
1503            $classCache[$lcName = $lcMethods[$i]] = $method;
1504
1505            if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) {
1506                $name = substr($method, 3);
1507                $lcName = substr($lcName, 3);
1508            } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) {
1509                $name = substr($method, 2);
1510                $lcName = substr($lcName, 2);
1511            } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) {
1512                $name = substr($method, 3);
1513                $lcName = substr($lcName, 3);
1514                if (\in_array('is'.$lcName, $lcMethods)) {
1515                    continue;
1516                }
1517            } else {
1518                continue;
1519            }
1520
1521            // skip get() and is() methods (in which case, $name is empty)
1522            if ($name) {
1523                if (!isset($classCache[$name])) {
1524                    $classCache[$name] = $method;
1525                }
1526
1527                if (!isset($classCache[$lcName])) {
1528                    $classCache[$lcName] = $method;
1529                }
1530            }
1531        }
1532        $cache[$class] = $classCache;
1533    }
1534
1535    $call = false;
1536    if (isset($cache[$class][$item])) {
1537        $method = $cache[$class][$item];
1538    } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) {
1539        $method = $cache[$class][$lcItem];
1540    } elseif (isset($cache[$class]['__call'])) {
1541        $method = $item;
1542        $call = true;
1543    } else {
1544        if ($isDefinedTest) {
1545            return false;
1546        }
1547
1548        if ($ignoreStrictCheck || !$env->isStrictVariables()) {
1549            return;
1550        }
1551
1552        throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
1553    }
1554
1555    if ($isDefinedTest) {
1556        return true;
1557    }
1558
1559    if ($sandboxed) {
1560        $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
1561    }
1562
1563    // Some objects throw exceptions when they have __call, and the method we try
1564    // to call is not supported. If ignoreStrictCheck is true, we should return null.
1565    try {
1566        $ret = $object->$method(...$arguments);
1567    } catch (\BadMethodCallException $e) {
1568        if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
1569            return;
1570        }
1571        throw $e;
1572    }
1573
1574    return $ret;
1575}
1576
1577/**
1578 * Returns the values from a single column in the input array.
1579 *
1580 * <pre>
1581 *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
1582 *
1583 *  {% set fruits = items|column('fruit') %}
1584 *
1585 *  {# fruits now contains ['apple', 'orange'] #}
1586 * </pre>
1587 *
1588 * @param array|Traversable $array An array
1589 * @param mixed             $name  The column name
1590 * @param mixed             $index The column to use as the index/keys for the returned array
1591 *
1592 * @return array The array of values
1593 */
1594function twig_array_column($array, $name, $index = null): array
1595{
1596    if ($array instanceof Traversable) {
1597        $array = iterator_to_array($array);
1598    } elseif (!\is_array($array)) {
1599        throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
1600    }
1601
1602    return array_column($array, $name, $index);
1603}
1604
1605function twig_array_filter(Environment $env, $array, $arrow)
1606{
1607    if (!twig_test_iterable($array)) {
1608        throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
1609    }
1610
1611    twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter');
1612
1613    if (\is_array($array)) {
1614        return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
1615    }
1616
1617    // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
1618    return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
1619}
1620
1621function twig_array_map(Environment $env, $array, $arrow)
1622{
1623    twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter');
1624
1625    $r = [];
1626    foreach ($array as $k => $v) {
1627        $r[$k] = $arrow($v, $k);
1628    }
1629
1630    return $r;
1631}
1632
1633function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
1634{
1635    twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter');
1636
1637    if (!\is_array($array)) {
1638        if (!$array instanceof \Traversable) {
1639            throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
1640        }
1641
1642        $array = iterator_to_array($array);
1643    }
1644
1645    return array_reduce($array, $arrow, $initial);
1646}
1647
1648function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
1649{
1650    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
1651        throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
1652    }
1653}
1654}
1655