Project

General

Profile

1
<?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
     * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object
52
     */
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
        if (null === $this->timezone) {
66
            $this->timezone = new DateTimeZone(date_default_timezone_get());
67
        }
68

    
69
        return $this->timezone;
70
    }
71

    
72
    /**
73
     * Sets the default format to be used by the number_format filter.
74
     *
75
     * @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
     */
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
            new Twig_TokenParser_Embed(),
117
        );
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
            'date_modify'   => new Twig_Filter_Function('twig_date_modify_filter', array('needs_environment' => true)),
131
            '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
            'abs'           => new Twig_Filter_Function('abs'),
135

    
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
            'split'   => new Twig_Filter_Function('twig_split_filter'),
153
            '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
                '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
                '=='     => 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
 * @param Twig_Environment                 $env    A Twig_Environment instance
323
 * @param Traversable|array|integer|string $values The values to pick a random item from
324
 *
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
 * @return string The formatted date
388
 */
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
    if ($date instanceof DateInterval) {
397
        return $date->format($format);
398
    }
399

    
400
    return twig_date_converter($env, $date, $timezone)->format($format);
401
}
402

    
403
/**
404
 * 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
 * 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
    // 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
    if ($date instanceof DateTime) {
451
        $date = clone $date;
452
        if (false !== $timezone) {
453
            $date->setTimezone($defaultTimezone);
454
        }
455

    
456
        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
        $date->setTimezone($defaultTimezone);
463

    
464
        return $date;
465
    }
466

    
467
    return new DateTime($date, $defaultTimezone);
468
}
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
 * @param integer             $decimal      The number of decimal points to display.
480
 * @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
/**
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
// 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
    $strict = is_object($value);
777

    
778
    if (is_array($compare)) {
779
        return in_array($value, $compare, $strict);
780
    } 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
        return in_array($value, iterator_to_array($compare, false), $strict);
788
    }
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
 * @param string           $strategy   The escaping strategy
799
 * @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
function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
803
{
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
    switch ($strategy) {
819
        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
            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
827
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
828
            }
829

    
830
            $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string);
831

    
832
            if ('UTF-8' != $charset) {
833
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
834
            }
835

    
836
            return $string;
837

    
838
        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
        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
            // top of the function slow downs other escaping strategies.
878
            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
        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
        default:
912
            throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: html, js, url, css, and html_attr).', $strategy));
913
    }
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
if (function_exists('mb_convert_encoding')) {
931
    function twig_convert_encoding($string, $to, $from)
932
    {
933
        return mb_convert_encoding($string, $to, $from);
934
    }
935
} elseif (function_exists('iconv')) {
936
    function twig_convert_encoding($string, $to, $from)
937
    {
938
        return iconv($from, $to, $string);
939
    }
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
        return '\\x'.strtoupper(substr('00'.bin2hex($char), -2));
954
    }
955

    
956
    // \uHHHH
957
    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
958

    
959
    return '\\u'.strtoupper(substr('0000'.bin2hex($char), -4));
960
}
961

    
962
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
// 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
else {
1124
    /**
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
}
(1-1/6)