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