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