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