Project

General

Profile

1 1686 darkviper
<?php
2
3
if (!defined('ENT_SUBSTITUTE')) {
4
    define('ENT_SUBSTITUTE', 8);
5
}
6
7
/*
8
 * This file is part of Twig.
9
 *
10
 * (c) 2009 Fabien Potencier
11
 *
12
 * For the full copyright and license information, please view the LICENSE
13
 * file that was distributed with this source code.
14
 */
15
class Twig_Extension_Core extends Twig_Extension
16
{
17
    protected $dateFormats = array('F j, Y H:i', '%d days');
18
    protected $numberFormat = array(0, '.', ',');
19
    protected $timezone = null;
20
21
    /**
22
     * Sets the default format to be used by the date filter.
23
     *
24
     * @param string $format             The default date format string
25
     * @param string $dateIntervalFormat The default date interval format string
26
     */
27
    public function setDateFormat($format = null, $dateIntervalFormat = null)
28
    {
29
        if (null !== $format) {
30
            $this->dateFormats[0] = $format;
31
        }
32
33
        if (null !== $dateIntervalFormat) {
34
            $this->dateFormats[1] = $dateIntervalFormat;
35
        }
36
    }
37
38
    /**
39
     * Gets the default format to be used by the date filter.
40
     *
41
     * @return array The default date format string and the default date interval format string
42
     */
43
    public function getDateFormat()
44
    {
45
        return $this->dateFormats;
46
    }
47
48
    /**
49
     * Sets the default timezone to be used by the date filter.
50
     *
51 1852 darkviper
     * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object
52 1686 darkviper
     */
53
    public function setTimezone($timezone)
54
    {
55
        $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
56
    }
57
58
    /**
59
     * Gets the default timezone to be used by the date filter.
60
     *
61
     * @return DateTimeZone The default timezone currently in use
62
     */
63
    public function getTimezone()
64
    {
65 1852 darkviper
        if (null === $this->timezone) {
66
            $this->timezone = new DateTimeZone(date_default_timezone_get());
67
        }
68
69 1686 darkviper
        return $this->timezone;
70
    }
71
72
    /**
73
     * Sets the default format to be used by the number_format filter.
74
     *
75 1852 darkviper
     * @param integer $decimal      The number of decimal places to use.
76
     * @param string  $decimalPoint The character(s) to use for the decimal point.
77
     * @param string  $thousandSep  The character(s) to use for the thousands separator.
78 1686 darkviper
     */
79
    public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
80
    {
81
        $this->numberFormat = array($decimal, $decimalPoint, $thousandSep);
82
    }
83
84
    /**
85
     * Get the default format used by the number_format filter.
86
     *
87
     * @return array The arguments for number_format()
88
     */
89
    public function getNumberFormat()
90
    {
91
        return $this->numberFormat;
92
    }
93
94
    /**
95
     * Returns the token parser instance to add to the existing list.
96
     *
97
     * @return array An array of Twig_TokenParser instances
98
     */
99
    public function getTokenParsers()
100
    {
101
        return array(
102
            new Twig_TokenParser_For(),
103
            new Twig_TokenParser_If(),
104
            new Twig_TokenParser_Extends(),
105
            new Twig_TokenParser_Include(),
106
            new Twig_TokenParser_Block(),
107
            new Twig_TokenParser_Use(),
108
            new Twig_TokenParser_Filter(),
109
            new Twig_TokenParser_Macro(),
110
            new Twig_TokenParser_Import(),
111
            new Twig_TokenParser_From(),
112
            new Twig_TokenParser_Set(),
113
            new Twig_TokenParser_Spaceless(),
114
            new Twig_TokenParser_Flush(),
115
            new Twig_TokenParser_Do(),
116 1852 darkviper
            new Twig_TokenParser_Embed(),
117 1686 darkviper
        );
118
    }
119
120
    /**
121
     * Returns a list of filters to add to the existing list.
122
     *
123
     * @return array An array of filters
124
     */
125
    public function getFilters()
126
    {
127
        $filters = array(
128
            // formatting filters
129
            'date'          => new Twig_Filter_Function('twig_date_format_filter', array('needs_environment' => true)),
130 1852 darkviper
            'date_modify'   => new Twig_Filter_Function('twig_date_modify_filter', array('needs_environment' => true)),
131 1686 darkviper
            'format'        => new Twig_Filter_Function('sprintf'),
132
            'replace'       => new Twig_Filter_Function('strtr'),
133
            'number_format' => new Twig_Filter_Function('twig_number_format_filter', array('needs_environment' => true)),
134 1852 darkviper
            'abs'           => new Twig_Filter_Function('abs'),
135 1686 darkviper
136
            // encoding
137
            'url_encode'       => new Twig_Filter_Function('twig_urlencode_filter'),
138
            'json_encode'      => new Twig_Filter_Function('twig_jsonencode_filter'),
139
            'convert_encoding' => new Twig_Filter_Function('twig_convert_encoding'),
140
141
            // string filters
142
            'title'      => new Twig_Filter_Function('twig_title_string_filter', array('needs_environment' => true)),
143
            'capitalize' => new Twig_Filter_Function('twig_capitalize_string_filter', array('needs_environment' => true)),
144
            'upper'      => new Twig_Filter_Function('strtoupper'),
145
            'lower'      => new Twig_Filter_Function('strtolower'),
146
            'striptags'  => new Twig_Filter_Function('strip_tags'),
147
            'trim'       => new Twig_Filter_Function('trim'),
148
            'nl2br'      => new Twig_Filter_Function('nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
149
150
            // array helpers
151
            'join'    => new Twig_Filter_Function('twig_join_filter'),
152 1852 darkviper
            'split'   => new Twig_Filter_Function('twig_split_filter'),
153 1686 darkviper
            'sort'    => new Twig_Filter_Function('twig_sort_filter'),
154
            'merge'   => new Twig_Filter_Function('twig_array_merge'),
155
156
            // string/array filters
157
            'reverse' => new Twig_Filter_Function('twig_reverse_filter', array('needs_environment' => true)),
158
            'length'  => new Twig_Filter_Function('twig_length_filter', array('needs_environment' => true)),
159
            'slice'   => new Twig_Filter_Function('twig_slice', array('needs_environment' => true)),
160
161
            // iteration and runtime
162
            'default' => new Twig_Filter_Node('Twig_Node_Expression_Filter_Default'),
163
            '_default' => new Twig_Filter_Function('_twig_default_filter'),
164
165
            'keys'    => new Twig_Filter_Function('twig_get_array_keys_filter'),
166
167
            // escaping
168
            'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
169
            'e'      => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
170
        );
171
172
        if (function_exists('mb_get_info')) {
173
            $filters['upper'] = new Twig_Filter_Function('twig_upper_filter', array('needs_environment' => true));
174
            $filters['lower'] = new Twig_Filter_Function('twig_lower_filter', array('needs_environment' => true));
175
        }
176
177
        return $filters;
178
    }
179
180
    /**
181
     * Returns a list of global functions to add to the existing list.
182
     *
183
     * @return array An array of global functions
184
     */
185
    public function getFunctions()
186
    {
187
        return array(
188
            'range'    => new Twig_Function_Function('range'),
189
            'constant' => new Twig_Function_Function('constant'),
190
            'cycle'    => new Twig_Function_Function('twig_cycle'),
191
            'random'   => new Twig_Function_Function('twig_random', array('needs_environment' => true)),
192
            'date'     => new Twig_Function_Function('twig_date_converter', array('needs_environment' => true)),
193
        );
194
    }
195
196
    /**
197
     * Returns a list of tests to add to the existing list.
198
     *
199
     * @return array An array of tests
200
     */
201
    public function getTests()
202
    {
203
        return array(
204
            'even'        => new Twig_Test_Node('Twig_Node_Expression_Test_Even'),
205
            'odd'         => new Twig_Test_Node('Twig_Node_Expression_Test_Odd'),
206
            'defined'     => new Twig_Test_Node('Twig_Node_Expression_Test_Defined'),
207
            'sameas'      => new Twig_Test_Node('Twig_Node_Expression_Test_Sameas'),
208
            'none'        => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
209
            'null'        => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
210
            'divisibleby' => new Twig_Test_Node('Twig_Node_Expression_Test_Divisibleby'),
211
            'constant'    => new Twig_Test_Node('Twig_Node_Expression_Test_Constant'),
212
            'empty'       => new Twig_Test_Function('twig_test_empty'),
213
            'iterable'    => new Twig_Test_Function('twig_test_iterable'),
214
        );
215
    }
216
217
    /**
218
     * Returns a list of operators to add to the existing list.
219
     *
220
     * @return array An array of operators
221
     */
222
    public function getOperators()
223
    {
224
        return array(
225
            array(
226
                'not' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
227
                '-'   => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Neg'),
228
                '+'   => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'),
229
            ),
230
            array(
231
                'or'     => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
232
                'and'    => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
233 1852 darkviper
                'b-or'   => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
234
                'b-xor'  => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
235
                'b-and'  => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
236 1686 darkviper
                '=='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
237
                '!='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
238
                '<'      => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
239
                '>'      => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Greater', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
240
                '>='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_GreaterEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
241
                '<='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
242
                'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
243
                'in'     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
244
                '..'     => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
245
                '+'      => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
246
                '-'      => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
247
                '~'      => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
248
                '*'      => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mul', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
249
                '/'      => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Div', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
250
                '//'     => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_FloorDiv', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
251
                '%'      => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
252
                'is'     => array('precedence' => 100, 'callable' => array($this, 'parseTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
253
                'is not' => array('precedence' => 100, 'callable' => array($this, 'parseNotTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
254
                '**'     => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT),
255
            ),
256
        );
257
    }
258
259
    public function parseNotTestExpression(Twig_Parser $parser, $node)
260
    {
261
        return new Twig_Node_Expression_Unary_Not($this->parseTestExpression($parser, $node), $parser->getCurrentToken()->getLine());
262
    }
263
264
    public function parseTestExpression(Twig_Parser $parser, $node)
265
    {
266
        $stream = $parser->getStream();
267
        $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
268
        $arguments = null;
269
        if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
270
            $arguments = $parser->getExpressionParser()->parseArguments();
271
        }
272
273
        $class = $this->getTestNodeClass($parser->getEnvironment(), $name);
274
275
        return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine());
276
    }
277
278
    protected function getTestNodeClass(Twig_Environment $env, $name)
279
    {
280
        $testMap = $env->getTests();
281
        if (isset($testMap[$name]) && $testMap[$name] instanceof Twig_Test_Node) {
282
            return $testMap[$name]->getClass();
283
        }
284
285
        return 'Twig_Node_Expression_Test';
286
    }
287
288
    /**
289
     * Returns the name of the extension.
290
     *
291
     * @return string The extension name
292
     */
293
    public function getName()
294
    {
295
        return 'core';
296
    }
297
}
298
299
/**
300
 * Cycles over a value.
301
 *
302
 * @param ArrayAccess|array $values An array or an ArrayAccess instance
303
 * @param integer           $i      The cycle value
304
 *
305
 * @return string The next value in the cycle
306
 */
307
function twig_cycle($values, $i)
308
{
309
    if (!is_array($values) && !$values instanceof ArrayAccess) {
310
        return $values;
311
    }
312
313
    return $values[$i % count($values)];
314
}
315
316
/**
317
 * Returns a random value depending on the supplied parameter type:
318
 * - a random item from a Traversable or array
319
 * - a random character from a string
320
 * - a random integer between 0 and the integer parameter
321
 *
322 1852 darkviper
 * @param Twig_Environment                 $env    A Twig_Environment instance
323
 * @param Traversable|array|integer|string $values The values to pick a random item from
324 1686 darkviper
 *
325
 * @throws Twig_Error_Runtime When $values is an empty array (does not apply to an empty string which is returned as is).
326
 *
327
 * @return mixed A random value from the given sequence
328
 */
329
function twig_random(Twig_Environment $env, $values = null)
330
{
331
    if (null === $values) {
332
        return mt_rand();
333
    }
334
335
    if (is_int($values) || is_float($values)) {
336
        return $values < 0 ? mt_rand($values, 0) : mt_rand(0, $values);
337
    }
338
339
    if ($values instanceof Traversable) {
340
        $values = iterator_to_array($values);
341
    } elseif (is_string($values)) {
342
        if ('' === $values) {
343
            return '';
344
        }
345
        if (null !== $charset = $env->getCharset()) {
346
            if ('UTF-8' != $charset) {
347
                $values = twig_convert_encoding($values, 'UTF-8', $charset);
348
            }
349
350
            // unicode version of str_split()
351
            // split at all positions, but not after the start and not before the end
352
            $values = preg_split('/(?<!^)(?!$)/u', $values);
353
354
            if ('UTF-8' != $charset) {
355
                foreach ($values as $i => $value) {
356
                    $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
357
                }
358
            }
359
        } else {
360
            return $values[mt_rand(0, strlen($values) - 1)];
361
        }
362
    }
363
364
    if (!is_array($values)) {
365
        return $values;
366
    }
367
368
    if (0 === count($values)) {
369
        throw new Twig_Error_Runtime('The random function cannot pick from an empty array.');
370
    }
371
372
    return $values[array_rand($values, 1)];
373
}
374
375
/**
376
 * Converts a date to the given format.
377
 *
378
 * <pre>
379
 *   {{ post.published_at|date("m/d/Y") }}
380
 * </pre>
381
 *
382
 * @param Twig_Environment             $env      A Twig_Environment instance
383
 * @param DateTime|DateInterval|string $date     A date
384
 * @param string                       $format   A format
385
 * @param DateTimeZone|string          $timezone A timezone
386
 *
387 1852 darkviper
 * @return string The formatted date
388 1686 darkviper
 */
389
function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null)
390
{
391
    if (null === $format) {
392
        $formats = $env->getExtension('core')->getDateFormat();
393
        $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
394
    }
395
396 1852 darkviper
    if ($date instanceof DateInterval) {
397 1686 darkviper
        return $date->format($format);
398
    }
399
400
    return twig_date_converter($env, $date, $timezone)->format($format);
401
}
402
403
/**
404 1852 darkviper
 * Returns a new date object modified
405
 *
406
 * <pre>
407
 *   {{ post.published_at|modify("-1day")|date("m/d/Y") }}
408
 * </pre>
409
 *
410
 * @param Twig_Environment  $env      A Twig_Environment instance
411
 * @param DateTime|string   $date     A date
412
 * @param string            $modifier A modifier string
413
 *
414
 * @return DateTime A new date object
415
 */
416
function twig_date_modify_filter(Twig_Environment $env, $date, $modifier)
417
{
418
    $date = twig_date_converter($env, $date, false);
419
    $date->modify($modifier);
420
421
    return $date;
422
}
423
424
/**
425 1686 darkviper
 * Converts an input to a DateTime instance.
426
 *
427
 * <pre>
428
 *    {% if date(user.created_at) < date('+2days') %}
429
 *      {# do something #}
430
 *    {% endif %}
431
 * </pre>
432
 *
433
 * @param Twig_Environment    $env      A Twig_Environment instance
434
 * @param DateTime|string     $date     A date
435
 * @param DateTimeZone|string $timezone A timezone
436
 *
437
 * @return DateTime A DateTime instance
438
 */
439
function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null)
440
{
441 1852 darkviper
    // determine the timezone
442
    if (!$timezone) {
443
        $defaultTimezone = $env->getExtension('core')->getTimezone();
444
    } elseif (!$timezone instanceof DateTimeZone) {
445
        $defaultTimezone = new DateTimeZone($timezone);
446
    } else {
447
        $defaultTimezone = $timezone;
448
    }
449
450 1686 darkviper
    if ($date instanceof DateTime) {
451 1852 darkviper
        $date = clone $date;
452
        if (false !== $timezone) {
453
            $date->setTimezone($defaultTimezone);
454
        }
455
456 1686 darkviper
        return $date;
457
    }
458
459
    $asString = (string) $date;
460
    if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
461
        $date = new DateTime('@'.$date);
462 1852 darkviper
        $date->setTimezone($defaultTimezone);
463 1686 darkviper
464 1852 darkviper
        return $date;
465 1686 darkviper
    }
466
467 1852 darkviper
    return new DateTime($date, $defaultTimezone);
468 1686 darkviper
}
469
470
/**
471
 * Number format filter.
472
 *
473
 * All of the formatting options can be left null, in that case the defaults will
474
 * be used.  Supplying any of the parameters will override the defaults set in the
475
 * environment object.
476
 *
477
 * @param Twig_Environment    $env          A Twig_Environment instance
478
 * @param mixed               $number       A float/int/string of the number to format
479 1852 darkviper
 * @param integer             $decimal      The number of decimal points to display.
480 1686 darkviper
 * @param string              $decimalPoint The character(s) to use for the decimal point.
481
 * @param string              $thousandSep  The character(s) to use for the thousands separator.
482
 *
483
 * @return string The formatted number
484
 */
485
function twig_number_format_filter(Twig_Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
486
{
487
    $defaults = $env->getExtension('core')->getNumberFormat();
488
    if (null === $decimal) {
489
        $decimal = $defaults[0];
490
    }
491
492
    if (null === $decimalPoint) {
493
        $decimalPoint = $defaults[1];
494
    }
495
496
    if (null === $thousandSep) {
497
        $thousandSep = $defaults[2];
498
    }
499
500
    return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
501
}
502
503
/**
504
 * URL encodes a string.
505
 *
506
 * @param string $url A URL
507
 * @param bool   $raw true to use rawurlencode() instead of urlencode
508
 *
509
 * @return string The URL encoded value
510
 */
511
function twig_urlencode_filter($url, $raw = false)
512
{
513
    if ($raw) {
514
        return rawurlencode($url);
515
    }
516
517
    return urlencode($url);
518
}
519
520
if (version_compare(PHP_VERSION, '5.3.0', '<')) {
521
    /**
522
     * JSON encodes a variable.
523
     *
524
     * @param mixed   $value   The value to encode.
525
     * @param integer $options Not used on PHP 5.2.x
526
     *
527
     * @return mixed The JSON encoded value
528
     */
529
    function twig_jsonencode_filter($value, $options = 0)
530
    {
531
        if ($value instanceof Twig_Markup) {
532
            $value = (string) $value;
533
        } elseif (is_array($value)) {
534
            array_walk_recursive($value, '_twig_markup2string');
535
        }
536
537
        return json_encode($value);
538
    }
539
} else {
540
    /**
541
     * JSON encodes a variable.
542
     *
543
     * @param mixed   $value   The value to encode.
544
     * @param integer $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
545
     *
546
     * @return mixed The JSON encoded value
547
     */
548
    function twig_jsonencode_filter($value, $options = 0)
549
    {
550
        if ($value instanceof Twig_Markup) {
551
            $value = (string) $value;
552
        } elseif (is_array($value)) {
553
            array_walk_recursive($value, '_twig_markup2string');
554
        }
555
556
        return json_encode($value, $options);
557
    }
558
}
559
560
function _twig_markup2string(&$value)
561
{
562
    if ($value instanceof Twig_Markup) {
563
        $value = (string) $value;
564
    }
565
}
566
567
/**
568
 * Merges an array with another one.
569
 *
570
 * <pre>
571
 *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
572
 *
573
 *  {% set items = items|merge({ 'peugeot': 'car' }) %}
574
 *
575
 *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
576
 * </pre>
577
 *
578
 * @param array $arr1 An array
579
 * @param array $arr2 An array
580
 *
581
 * @return array The merged array
582
 */
583
function twig_array_merge($arr1, $arr2)
584
{
585
    if (!is_array($arr1) || !is_array($arr2)) {
586
        throw new Twig_Error_Runtime('The merge filter only works with arrays or hashes.');
587
    }
588
589
    return array_merge($arr1, $arr2);
590
}
591
592
/**
593
 * Slices a variable.
594
 *
595
 * @param Twig_Environment $env          A Twig_Environment instance
596
 * @param mixed            $item         A variable
597
 * @param integer          $start        Start of the slice
598
 * @param integer          $length       Size of the slice
599
 * @param Boolean          $preserveKeys Whether to preserve key or not (when the input is an array)
600
 *
601
 * @return mixed The sliced variable
602
 */
603
function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false)
604
{
605
    if ($item instanceof Traversable) {
606
        $item = iterator_to_array($item, false);
607
    }
608
609
    if (is_array($item)) {
610
        return array_slice($item, $start, $length, $preserveKeys);
611
    }
612
613
    $item = (string) $item;
614
615
    if (function_exists('mb_get_info') && null !== $charset = $env->getCharset()) {
616
        return mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset);
617
    }
618
619
    return null === $length ? substr($item, $start) : substr($item, $start, $length);
620
}
621
622
/**
623
 * Joins the values to a string.
624
 *
625
 * The separator between elements is an empty string per default, you can define it with the optional parameter.
626
 *
627
 * <pre>
628
 *  {{ [1, 2, 3]|join('|') }}
629
 *  {# returns 1|2|3 #}
630
 *
631
 *  {{ [1, 2, 3]|join }}
632
 *  {# returns 123 #}
633
 * </pre>
634
 *
635
 * @param array  $value An array
636
 * @param string $glue  The separator
637
 *
638
 * @return string The concatenated string
639
 */
640
function twig_join_filter($value, $glue = '')
641
{
642
    if ($value instanceof Traversable) {
643
        $value = iterator_to_array($value, false);
644
    }
645
646
    return implode($glue, (array) $value);
647
}
648
649 1852 darkviper
/**
650
 * Splits the string into an array.
651
 *
652
 * <pre>
653
 *  {{ "one,two,three"|split(',') }}
654
 *  {# returns [one, two, three] #}
655
 *
656
 *  {{ "one,two,three,four,five"|split(',', 3) }}
657
 *  {# returns [one, two, "three,four,five"] #}
658
 *
659
 *  {{ "123"|split('') }}
660
 *  {# returns [1, 2, 3] #}
661
 *
662
 *  {{ "aabbcc"|split('', 2) }}
663
 *  {# returns [aa, bb, cc] #}
664
 * </pre>
665
 *
666
 * @param string  $value     A string
667
 * @param string  $delimiter The delimiter
668
 * @param integer $limit     The limit
669
 *
670
 * @return array The split string as an array
671
 */
672
function twig_split_filter($value, $delimiter, $limit = null)
673
{
674
    if (empty($delimiter)) {
675
        return str_split($value, null === $limit ? 1 : $limit);
676
    }
677
678
    return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
679
}
680
681 1686 darkviper
// The '_default' filter is used internally to avoid using the ternary operator
682
// which costs a lot for big contexts (before PHP 5.4). So, on average,
683
// a function call is cheaper.
684
function _twig_default_filter($value, $default = '')
685
{
686
    if (twig_test_empty($value)) {
687
        return $default;
688
    }
689
690
    return $value;
691
}
692
693
/**
694
 * Returns the keys for the given array.
695
 *
696
 * It is useful when you want to iterate over the keys of an array:
697
 *
698
 * <pre>
699
 *  {% for key in array|keys %}
700
 *      {# ... #}
701
 *  {% endfor %}
702
 * </pre>
703
 *
704
 * @param array $array An array
705
 *
706
 * @return array The keys
707
 */
708
function twig_get_array_keys_filter($array)
709
{
710
    if (is_object($array) && $array instanceof Traversable) {
711
        return array_keys(iterator_to_array($array));
712
    }
713
714
    if (!is_array($array)) {
715
        return array();
716
    }
717
718
    return array_keys($array);
719
}
720
721
/**
722
 * Reverses a variable.
723
 *
724
 * @param Twig_Environment         $env          A Twig_Environment instance
725
 * @param array|Traversable|string $item         An array, a Traversable instance, or a string
726
 * @param Boolean                  $preserveKeys Whether to preserve key or not
727
 *
728
 * @return mixed The reversed input
729
 */
730
function twig_reverse_filter(Twig_Environment $env, $item, $preserveKeys = false)
731
{
732
    if (is_object($item) && $item instanceof Traversable) {
733
        return array_reverse(iterator_to_array($item), $preserveKeys);
734
    }
735
736
    if (is_array($item)) {
737
        return array_reverse($item, $preserveKeys);
738
    }
739
740
    if (null !== $charset = $env->getCharset()) {
741
        $string = (string) $item;
742
743
        if ('UTF-8' != $charset) {
744
            $item = twig_convert_encoding($string, 'UTF-8', $charset);
745
        }
746
747
        preg_match_all('/./us', $item, $matches);
748
749
        $string = implode('', array_reverse($matches[0]));
750
751
        if ('UTF-8' != $charset) {
752
            $string = twig_convert_encoding($string, $charset, 'UTF-8');
753
        }
754
755
        return $string;
756
    }
757
758
    return strrev((string) $item);
759
}
760
761
/**
762
 * Sorts an array.
763
 *
764
 * @param array $array An array
765
 */
766
function twig_sort_filter($array)
767
{
768
    asort($array);
769
770
    return $array;
771
}
772
773
/* used internally */
774
function twig_in_filter($value, $compare)
775
{
776 1852 darkviper
    $strict = is_object($value);
777
778 1686 darkviper
    if (is_array($compare)) {
779 1852 darkviper
        return in_array($value, $compare, $strict);
780 1686 darkviper
    } elseif (is_string($compare)) {
781
        if (!strlen((string) $value)) {
782
            return empty($compare);
783
        }
784
785
        return false !== strpos($compare, (string) $value);
786
    } elseif (is_object($compare) && $compare instanceof Traversable) {
787 1852 darkviper
        return in_array($value, iterator_to_array($compare, false), $strict);
788 1686 darkviper
    }
789
790
    return false;
791
}
792
793
/**
794
 * Escapes a string.
795
 *
796
 * @param Twig_Environment $env        A Twig_Environment instance
797
 * @param string           $string     The value to be escaped
798 1852 darkviper
 * @param string           $strategy   The escaping strategy
799 1686 darkviper
 * @param string           $charset    The charset
800
 * @param Boolean          $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
801
 */
802 1852 darkviper
function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
803 1686 darkviper
{
804
    if ($autoescape && is_object($string) && $string instanceof Twig_Markup) {
805
        return $string;
806
    }
807
808
    if (!is_string($string) && !(is_object($string) && method_exists($string, '__toString'))) {
809
        return $string;
810
    }
811
812
    if (null === $charset) {
813
        $charset = $env->getCharset();
814
    }
815
816
    $string = (string) $string;
817
818 1852 darkviper
    switch ($strategy) {
819 1686 darkviper
        case 'js':
820
            // escape all non-alphanumeric characters
821
            // into their \xHH or \uHHHH representations
822
            if ('UTF-8' != $charset) {
823
                $string = twig_convert_encoding($string, 'UTF-8', $charset);
824
            }
825
826 1852 darkviper
            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
827 1686 darkviper
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
828
            }
829
830 1852 darkviper
            $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string);
831
832 1686 darkviper
            if ('UTF-8' != $charset) {
833
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
834
            }
835
836
            return $string;
837
838 1852 darkviper
        case 'css':
839
            if ('UTF-8' != $charset) {
840
                $string = twig_convert_encoding($string, 'UTF-8', $charset);
841
            }
842
843
            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
844
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
845
            }
846
847
            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string);
848
849
            if ('UTF-8' != $charset) {
850
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
851
            }
852
853
            return $string;
854
855
        case 'html_attr':
856
            if ('UTF-8' != $charset) {
857
                $string = twig_convert_encoding($string, 'UTF-8', $charset);
858
            }
859
860
            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
861
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
862
            }
863
864
            $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string);
865
866
            if ('UTF-8' != $charset) {
867
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
868
            }
869
870
            return $string;
871
872 1686 darkviper
        case 'html':
873
            // see http://php.net/htmlspecialchars
874
875
            // Using a static variable to avoid initializing the array
876
            // each time the function is called. Moving the declaration on the
877 1852 darkviper
            // top of the function slow downs other escaping strategies.
878 1686 darkviper
            static $htmlspecialcharsCharsets = array(
879
                'iso-8859-1' => true, 'iso8859-1' => true,
880
                'iso-8859-15' => true, 'iso8859-15' => true,
881
                'utf-8' => true,
882
                'cp866' => true, 'ibm866' => true, '866' => true,
883
                'cp1251' => true, 'windows-1251' => true, 'win-1251' => true,
884
                '1251' => true,
885
                'cp1252' => true, 'windows-1252' => true, '1252' => true,
886
                'koi8-r' => true, 'koi8-ru' => true, 'koi8r' => true,
887
                'big5' => true, '950' => true,
888
                'gb2312' => true, '936' => true,
889
                'big5-hkscs' => true,
890
                'shift_jis' => true, 'sjis' => true, '932' => true,
891
                'euc-jp' => true, 'eucjp' => true,
892
                'iso8859-5' => true, 'iso-8859-5' => true, 'macroman' => true,
893
            );
894
895
            if (isset($htmlspecialcharsCharsets[strtolower($charset)])) {
896
                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
897
            }
898
899
            $string = twig_convert_encoding($string, 'UTF-8', $charset);
900
            $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
901
902
            return twig_convert_encoding($string, $charset, 'UTF-8');
903
904 1852 darkviper
        case 'url':
905
            if (version_compare(PHP_VERSION, '5.3.0', '<')) {
906
                return str_replace('%7E', '~', rawurlencode($string));
907
            }
908
909
            return rawurlencode($string);
910
911 1686 darkviper
        default:
912 1852 darkviper
            throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: html, js, url, css, and html_attr).', $strategy));
913 1686 darkviper
    }
914
}
915
916
/* used internally */
917
function twig_escape_filter_is_safe(Twig_Node $filterArgs)
918
{
919
    foreach ($filterArgs as $arg) {
920
        if ($arg instanceof Twig_Node_Expression_Constant) {
921
            return array($arg->getAttribute('value'));
922
        }
923
924
        return array();
925
    }
926
927
    return array('html');
928
}
929
930 1852 darkviper
if (function_exists('mb_convert_encoding')) {
931 1686 darkviper
    function twig_convert_encoding($string, $to, $from)
932
    {
933 1852 darkviper
        return mb_convert_encoding($string, $to, $from);
934 1686 darkviper
    }
935 1852 darkviper
} elseif (function_exists('iconv')) {
936 1686 darkviper
    function twig_convert_encoding($string, $to, $from)
937
    {
938 1852 darkviper
        return iconv($from, $to, $string);
939 1686 darkviper
    }
940
} else {
941
    function twig_convert_encoding($string, $to, $from)
942
    {
943
        throw new Twig_Error_Runtime('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
944
    }
945
}
946
947
function _twig_escape_js_callback($matches)
948
{
949
    $char = $matches[0];
950
951
    // \xHH
952
    if (!isset($char[1])) {
953 1852 darkviper
        return '\\x'.strtoupper(substr('00'.bin2hex($char), -2));
954 1686 darkviper
    }
955
956
    // \uHHHH
957
    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
958
959 1852 darkviper
    return '\\u'.strtoupper(substr('0000'.bin2hex($char), -4));
960 1686 darkviper
}
961
962 1852 darkviper
function _twig_escape_css_callback($matches)
963
{
964
    $char = $matches[0];
965
966
    // \xHH
967
    if (!isset($char[1])) {
968
        $hex = ltrim(strtoupper(bin2hex($char)), '0');
969
        if (0 === strlen($hex)) {
970
            $hex = '0';
971
        }
972
973
        return '\\'.$hex.' ';
974
    }
975
976
    // \uHHHH
977
    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
978
979
    return '\\'.ltrim(strtoupper(bin2hex($char)), '0').' ';
980
}
981
982
/**
983
 * This function is adapted from code coming from Zend Framework.
984
 *
985
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
986
 * @license   http://framework.zend.com/license/new-bsd New BSD License
987
 */
988
function _twig_escape_html_attr_callback($matches)
989
{
990
    /*
991
     * While HTML supports far more named entities, the lowest common denominator
992
     * has become HTML5's XML Serialisation which is restricted to the those named
993
     * entities that XML supports. Using HTML entities would result in this error:
994
     *     XML Parsing Error: undefined entity
995
     */
996
    static $entityMap = array(
997
        34 => 'quot', /* quotation mark */
998
        38 => 'amp',  /* ampersand */
999
        60 => 'lt',   /* less-than sign */
1000
        62 => 'gt',   /* greater-than sign */
1001
    );
1002
1003
    $chr = $matches[0];
1004
    $ord = ord($chr);
1005
1006
    /**
1007
     * The following replaces characters undefined in HTML with the
1008
     * hex entity for the Unicode replacement character.
1009
     */
1010
    if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) {
1011
        return '&#xFFFD;';
1012
    }
1013
1014
    /**
1015
     * Check if the current character to escape has a name entity we should
1016
     * replace it with while grabbing the hex value of the character.
1017
     */
1018
    if (strlen($chr) == 1) {
1019
        $hex = strtoupper(substr('00'.bin2hex($chr), -2));
1020
    } else {
1021
        $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8');
1022
        $hex = strtoupper(substr('0000'.bin2hex($chr), -4));
1023
    }
1024
1025
    $int = hexdec($hex);
1026
    if (array_key_exists($int, $entityMap)) {
1027
        return sprintf('&%s;', $entityMap[$int]);
1028
    }
1029
1030
    /**
1031
     * Per OWASP recommendations, we'll use hex entities for any other
1032
     * characters where a named entity does not exist.
1033
     */
1034
1035
    return sprintf('&#x%s;', $hex);
1036
}
1037
1038 1686 darkviper
// add multibyte extensions if possible
1039
if (function_exists('mb_get_info')) {
1040
    /**
1041
     * Returns the length of a variable.
1042
     *
1043
     * @param Twig_Environment $env   A Twig_Environment instance
1044
     * @param mixed            $thing A variable
1045
     *
1046
     * @return integer The length of the value
1047
     */
1048
    function twig_length_filter(Twig_Environment $env, $thing)
1049
    {
1050
        return is_scalar($thing) ? mb_strlen($thing, $env->getCharset()) : count($thing);
1051
    }
1052
1053
    /**
1054
     * Converts a string to uppercase.
1055
     *
1056
     * @param Twig_Environment $env    A Twig_Environment instance
1057
     * @param string           $string A string
1058
     *
1059
     * @return string The uppercased string
1060
     */
1061
    function twig_upper_filter(Twig_Environment $env, $string)
1062
    {
1063
        if (null !== ($charset = $env->getCharset())) {
1064
            return mb_strtoupper($string, $charset);
1065
        }
1066
1067
        return strtoupper($string);
1068
    }
1069
1070
    /**
1071
     * Converts a string to lowercase.
1072
     *
1073
     * @param Twig_Environment $env    A Twig_Environment instance
1074
     * @param string           $string A string
1075
     *
1076
     * @return string The lowercased string
1077
     */
1078
    function twig_lower_filter(Twig_Environment $env, $string)
1079
    {
1080
        if (null !== ($charset = $env->getCharset())) {
1081
            return mb_strtolower($string, $charset);
1082
        }
1083
1084
        return strtolower($string);
1085
    }
1086
1087
    /**
1088
     * Returns a titlecased string.
1089
     *
1090
     * @param Twig_Environment $env    A Twig_Environment instance
1091
     * @param string           $string A string
1092
     *
1093
     * @return string The titlecased string
1094
     */
1095
    function twig_title_string_filter(Twig_Environment $env, $string)
1096
    {
1097
        if (null !== ($charset = $env->getCharset())) {
1098
            return mb_convert_case($string, MB_CASE_TITLE, $charset);
1099
        }
1100
1101
        return ucwords(strtolower($string));
1102
    }
1103
1104
    /**
1105
     * Returns a capitalized string.
1106
     *
1107
     * @param Twig_Environment $env    A Twig_Environment instance
1108
     * @param string           $string A string
1109
     *
1110
     * @return string The capitalized string
1111
     */
1112
    function twig_capitalize_string_filter(Twig_Environment $env, $string)
1113
    {
1114
        if (null !== ($charset = $env->getCharset())) {
1115
            return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).
1116
                         mb_strtolower(mb_substr($string, 1, mb_strlen($string, $charset), $charset), $charset);
1117
        }
1118
1119
        return ucfirst(strtolower($string));
1120
    }
1121
}
1122
// and byte fallback
1123 1852 darkviper
else {
1124 1686 darkviper
    /**
1125
     * Returns the length of a variable.
1126
     *
1127
     * @param Twig_Environment $env   A Twig_Environment instance
1128
     * @param mixed            $thing A variable
1129
     *
1130
     * @return integer The length of the value
1131
     */
1132
    function twig_length_filter(Twig_Environment $env, $thing)
1133
    {
1134
        return is_scalar($thing) ? strlen($thing) : count($thing);
1135
    }
1136
1137
    /**
1138
     * Returns a titlecased string.
1139
     *
1140
     * @param Twig_Environment $env    A Twig_Environment instance
1141
     * @param string           $string A string
1142
     *
1143
     * @return string The titlecased string
1144
     */
1145
    function twig_title_string_filter(Twig_Environment $env, $string)
1146
    {
1147
        return ucwords(strtolower($string));
1148
    }
1149
1150
    /**
1151
     * Returns a capitalized string.
1152
     *
1153
     * @param Twig_Environment $env    A Twig_Environment instance
1154
     * @param string           $string A string
1155
     *
1156
     * @return string The capitalized string
1157
     */
1158
    function twig_capitalize_string_filter(Twig_Environment $env, $string)
1159
    {
1160
        return ucfirst(strtolower($string));
1161
    }
1162
}
1163
1164
/* used internally */
1165
function twig_ensure_traversable($seq)
1166
{
1167
    if ($seq instanceof Traversable || is_array($seq)) {
1168
        return $seq;
1169
    }
1170
1171
    return array();
1172
}
1173
1174
/**
1175
 * Checks if a variable is empty.
1176
 *
1177
 * <pre>
1178
 * {# evaluates to true if the foo variable is null, false, or the empty string #}
1179
 * {% if foo is empty %}
1180
 *     {# ... #}
1181
 * {% endif %}
1182
 * </pre>
1183
 *
1184
 * @param mixed $value A variable
1185
 *
1186
 * @return Boolean true if the value is empty, false otherwise
1187
 */
1188
function twig_test_empty($value)
1189
{
1190
    if ($value instanceof Countable) {
1191
        return 0 == count($value);
1192
    }
1193
1194
    return false === $value || (empty($value) && '0' != $value);
1195
}
1196
1197
/**
1198
 * Checks if a variable is traversable.
1199
 *
1200
 * <pre>
1201
 * {# evaluates to true if the foo variable is an array or a traversable object #}
1202
 * {% if foo is traversable %}
1203
 *     {# ... #}
1204
 * {% endif %}
1205
 * </pre>
1206
 *
1207
 * @param mixed $value A variable
1208
 *
1209
 * @return Boolean true if the value is traversable
1210
 */
1211
function twig_test_iterable($value)
1212
{
1213
    return $value instanceof Traversable || is_array($value);
1214
}