Project

General

Profile

1
<?php
2

    
3
if (!defined('ENT_SUBSTITUTE')) {
4
    // use 0 as hhvm does not support several flags yet
5
    define('ENT_SUBSTITUTE', 0);
6
}
7

    
8
/*
9
 * This file is part of Twig.
10
 *
11
 * (c) 2009 Fabien Potencier
12
 *
13
 * For the full copyright and license information, please view the LICENSE
14
 * file that was distributed with this source code.
15
 */
16
class Twig_Extension_Core extends Twig_Extension
17
{
18
    protected $dateFormats = array('F j, Y H:i', '%d days');
19
    protected $numberFormat = array(0, '.', ',');
20
    protected $timezone = null;
21
    protected $escapers = array();
22

    
23
    /**
24
     * Defines a new escaper to be used via the escape filter.
25
     *
26
     * @param string   $strategy The strategy name that should be used as a strategy in the escape call
27
     * @param callable $callable A valid PHP callable
28
     */
29
    public function setEscaper($strategy, $callable)
30
    {
31
        $this->escapers[$strategy] = $callable;
32
    }
33

    
34
    /**
35
     * Gets all defined escapers.
36
     *
37
     * @return array An array of escapers
38
     */
39
    public function getEscapers()
40
    {
41
        return $this->escapers;
42
    }
43

    
44
    /**
45
     * Sets the default format to be used by the date filter.
46
     *
47
     * @param string $format             The default date format string
48
     * @param string $dateIntervalFormat The default date interval format string
49
     */
50
    public function setDateFormat($format = null, $dateIntervalFormat = null)
51
    {
52
        if (null !== $format) {
53
            $this->dateFormats[0] = $format;
54
        }
55

    
56
        if (null !== $dateIntervalFormat) {
57
            $this->dateFormats[1] = $dateIntervalFormat;
58
        }
59
    }
60

    
61
    /**
62
     * Gets the default format to be used by the date filter.
63
     *
64
     * @return array The default date format string and the default date interval format string
65
     */
66
    public function getDateFormat()
67
    {
68
        return $this->dateFormats;
69
    }
70

    
71
    /**
72
     * Sets the default timezone to be used by the date filter.
73
     *
74
     * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object
75
     */
76
    public function setTimezone($timezone)
77
    {
78
        $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
79
    }
80

    
81
    /**
82
     * Gets the default timezone to be used by the date filter.
83
     *
84
     * @return DateTimeZone The default timezone currently in use
85
     */
86
    public function getTimezone()
87
    {
88
        if (null === $this->timezone) {
89
            $this->timezone = new DateTimeZone(date_default_timezone_get());
90
        }
91

    
92
        return $this->timezone;
93
    }
94

    
95
    /**
96
     * Sets the default format to be used by the number_format filter.
97
     *
98
     * @param int    $decimal      The number of decimal places to use.
99
     * @param string $decimalPoint The character(s) to use for the decimal point.
100
     * @param string $thousandSep  The character(s) to use for the thousands separator.
101
     */
102
    public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
103
    {
104
        $this->numberFormat = array($decimal, $decimalPoint, $thousandSep);
105
    }
106

    
107
    /**
108
     * Get the default format used by the number_format filter.
109
     *
110
     * @return array The arguments for number_format()
111
     */
112
    public function getNumberFormat()
113
    {
114
        return $this->numberFormat;
115
    }
116

    
117
    public function getTokenParsers()
118
    {
119
        return array(
120
            new Twig_TokenParser_For(),
121
            new Twig_TokenParser_If(),
122
            new Twig_TokenParser_Extends(),
123
            new Twig_TokenParser_Include(),
124
            new Twig_TokenParser_Block(),
125
            new Twig_TokenParser_Use(),
126
            new Twig_TokenParser_Filter(),
127
            new Twig_TokenParser_Macro(),
128
            new Twig_TokenParser_Import(),
129
            new Twig_TokenParser_From(),
130
            new Twig_TokenParser_Set(),
131
            new Twig_TokenParser_Spaceless(),
132
            new Twig_TokenParser_Flush(),
133
            new Twig_TokenParser_Do(),
134
            new Twig_TokenParser_Embed(),
135
        );
136
    }
137

    
138
    public function getFilters()
139
    {
140
        $filters = array(
141
            // formatting filters
142
            new Twig_SimpleFilter('date', 'twig_date_format_filter', array('needs_environment' => true)),
143
            new Twig_SimpleFilter('date_modify', 'twig_date_modify_filter', array('needs_environment' => true)),
144
            new Twig_SimpleFilter('format', 'sprintf'),
145
            new Twig_SimpleFilter('replace', 'twig_replace_filter'),
146
            new Twig_SimpleFilter('number_format', 'twig_number_format_filter', array('needs_environment' => true)),
147
            new Twig_SimpleFilter('abs', 'abs'),
148
            new Twig_SimpleFilter('round', 'twig_round'),
149

    
150
            // encoding
151
            new Twig_SimpleFilter('url_encode', 'twig_urlencode_filter'),
152
            new Twig_SimpleFilter('json_encode', 'twig_jsonencode_filter'),
153
            new Twig_SimpleFilter('convert_encoding', 'twig_convert_encoding'),
154

    
155
            // string filters
156
            new Twig_SimpleFilter('title', 'twig_title_string_filter', array('needs_environment' => true)),
157
            new Twig_SimpleFilter('capitalize', 'twig_capitalize_string_filter', array('needs_environment' => true)),
158
            new Twig_SimpleFilter('upper', 'strtoupper'),
159
            new Twig_SimpleFilter('lower', 'strtolower'),
160
            new Twig_SimpleFilter('striptags', 'strip_tags'),
161
            new Twig_SimpleFilter('trim', 'trim'),
162
            new Twig_SimpleFilter('nl2br', 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
163

    
164
            // array helpers
165
            new Twig_SimpleFilter('join', 'twig_join_filter'),
166
            new Twig_SimpleFilter('split', 'twig_split_filter', array('needs_environment' => true)),
167
            new Twig_SimpleFilter('sort', 'twig_sort_filter'),
168
            new Twig_SimpleFilter('merge', 'twig_array_merge'),
169
            new Twig_SimpleFilter('batch', 'twig_array_batch'),
170

    
171
            // string/array filters
172
            new Twig_SimpleFilter('reverse', 'twig_reverse_filter', array('needs_environment' => true)),
173
            new Twig_SimpleFilter('length', 'twig_length_filter', array('needs_environment' => true)),
174
            new Twig_SimpleFilter('slice', 'twig_slice', array('needs_environment' => true)),
175
            new Twig_SimpleFilter('first', 'twig_first', array('needs_environment' => true)),
176
            new Twig_SimpleFilter('last', 'twig_last', array('needs_environment' => true)),
177

    
178
            // iteration and runtime
179
            new Twig_SimpleFilter('default', '_twig_default_filter', array('node_class' => 'Twig_Node_Expression_Filter_Default')),
180
            new Twig_SimpleFilter('keys', 'twig_get_array_keys_filter'),
181

    
182
            // escaping
183
            new Twig_SimpleFilter('escape', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
184
            new Twig_SimpleFilter('e', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
185
        );
186

    
187
        if (function_exists('mb_get_info')) {
188
            $filters[] = new Twig_SimpleFilter('upper', 'twig_upper_filter', array('needs_environment' => true));
189
            $filters[] = new Twig_SimpleFilter('lower', 'twig_lower_filter', array('needs_environment' => true));
190
        }
191

    
192
        return $filters;
193
    }
194

    
195
    public function getFunctions()
196
    {
197
        return array(
198
            new Twig_SimpleFunction('max', 'max'),
199
            new Twig_SimpleFunction('min', 'min'),
200
            new Twig_SimpleFunction('range', 'range'),
201
            new Twig_SimpleFunction('constant', 'twig_constant'),
202
            new Twig_SimpleFunction('cycle', 'twig_cycle'),
203
            new Twig_SimpleFunction('random', 'twig_random', array('needs_environment' => true)),
204
            new Twig_SimpleFunction('date', 'twig_date_converter', array('needs_environment' => true)),
205
            new Twig_SimpleFunction('include', 'twig_include', array('needs_environment' => true, 'needs_context' => true, 'is_safe' => array('all'))),
206
            new Twig_SimpleFunction('source', 'twig_source', array('needs_environment' => true, 'is_safe' => array('all'))),
207
        );
208
    }
209

    
210
    public function getTests()
211
    {
212
        return array(
213
            new Twig_SimpleTest('even', null, array('node_class' => 'Twig_Node_Expression_Test_Even')),
214
            new Twig_SimpleTest('odd', null, array('node_class' => 'Twig_Node_Expression_Test_Odd')),
215
            new Twig_SimpleTest('defined', null, array('node_class' => 'Twig_Node_Expression_Test_Defined')),
216
            new Twig_SimpleTest('sameas', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas', 'deprecated' => '1.21', 'alternative' => 'same as')),
217
            new Twig_SimpleTest('same as', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas')),
218
            new Twig_SimpleTest('none', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
219
            new Twig_SimpleTest('null', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
220
            new Twig_SimpleTest('divisibleby', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby', 'deprecated' => '1.21', 'alternative' => 'divisible by')),
221
            new Twig_SimpleTest('divisible by', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby')),
222
            new Twig_SimpleTest('constant', null, array('node_class' => 'Twig_Node_Expression_Test_Constant')),
223
            new Twig_SimpleTest('empty', 'twig_test_empty'),
224
            new Twig_SimpleTest('iterable', 'twig_test_iterable'),
225
        );
226
    }
227

    
228
    public function getOperators()
229
    {
230
        return array(
231
            array(
232
                'not' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
233
                '-' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Neg'),
234
                '+' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'),
235
            ),
236
            array(
237
                'or' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
238
                'and' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
239
                'b-or' => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
240
                'b-xor' => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
241
                'b-and' => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
242
                '==' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
243
                '!=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
244
                '<' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
245
                '>' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Greater', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
246
                '>=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_GreaterEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
247
                '<=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
248
                'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
249
                'in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
250
                'matches' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Matches', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
251
                'starts with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_StartsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
252
                'ends with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_EndsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
253
                '..' => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
254
                '+' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
255
                '-' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
256
                '~' => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
257
                '*' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mul', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
258
                '/' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Div', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
259
                '//' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_FloorDiv', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
260
                '%' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
261
                'is' => array('precedence' => 100, 'callable' => array($this, 'parseTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
262
                'is not' => array('precedence' => 100, 'callable' => array($this, 'parseNotTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
263
                '**' => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT),
264
                '??' => array('precedence' => 300, 'class' => 'Twig_Node_Expression_NullCoalesce', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT),
265
            ),
266
        );
267
    }
268

    
269
    public function parseNotTestExpression(Twig_Parser $parser, Twig_NodeInterface $node)
270
    {
271
        return new Twig_Node_Expression_Unary_Not($this->parseTestExpression($parser, $node), $parser->getCurrentToken()->getLine());
272
    }
273

    
274
    public function parseTestExpression(Twig_Parser $parser, Twig_NodeInterface $node)
275
    {
276
        $stream = $parser->getStream();
277
        list($name, $test) = $this->getTest($parser, $node->getLine());
278

    
279
        if ($test instanceof Twig_SimpleTest && $test->isDeprecated()) {
280
            $message = sprintf('Twig Test "%s" is deprecated', $name);
281
            if (!is_bool($test->getDeprecatedVersion())) {
282
                $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
283
            }
284
            if ($test->getAlternative()) {
285
                $message .= sprintf('. Use "%s" instead', $test->getAlternative());
286
            }
287
            $message .= sprintf(' in %s at line %d.', $stream->getFilename(), $stream->getCurrent()->getLine());
288

    
289
            @trigger_error($message, E_USER_DEPRECATED);
290
        }
291

    
292
        $class = $this->getTestNodeClass($parser, $test);
293
        $arguments = null;
294
        if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
295
            $arguments = $parser->getExpressionParser()->parseArguments(true);
296
        }
297

    
298
        return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine());
299
    }
300

    
301
    protected function getTest(Twig_Parser $parser, $line)
302
    {
303
        $stream = $parser->getStream();
304
        $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
305
        $env = $parser->getEnvironment();
306

    
307
        if ($test = $env->getTest($name)) {
308
            return array($name, $test);
309
        }
310

    
311
        if ($stream->test(Twig_Token::NAME_TYPE)) {
312
            // try 2-words tests
313
            $name = $name.' '.$parser->getCurrentToken()->getValue();
314

    
315
            if ($test = $env->getTest($name)) {
316
                $parser->getStream()->next();
317

    
318
                return array($name, $test);
319
            }
320
        }
321

    
322
        $e = new Twig_Error_Syntax(sprintf('Unknown "%s" test.', $name), $line, $parser->getFilename());
323
        $e->addSuggestions($name, array_keys($env->getTests()));
324

    
325
        throw $e;
326
    }
327

    
328
    protected function getTestNodeClass(Twig_Parser $parser, $test)
329
    {
330
        if ($test instanceof Twig_SimpleTest) {
331
            return $test->getNodeClass();
332
        }
333

    
334
        return $test instanceof Twig_Test_Node ? $test->getClass() : 'Twig_Node_Expression_Test';
335
    }
336

    
337
    public function getName()
338
    {
339
        return 'core';
340
    }
341
}
342

    
343
/**
344
 * Cycles over a value.
345
 *
346
 * @param ArrayAccess|array $values   An array or an ArrayAccess instance
347
 * @param int               $position The cycle position
348
 *
349
 * @return string The next value in the cycle
350
 */
351
function twig_cycle($values, $position)
352
{
353
    if (!is_array($values) && !$values instanceof ArrayAccess) {
354
        return $values;
355
    }
356

    
357
    return $values[$position % count($values)];
358
}
359

    
360
/**
361
 * Returns a random value depending on the supplied parameter type:
362
 * - a random item from a Traversable or array
363
 * - a random character from a string
364
 * - a random integer between 0 and the integer parameter.
365
 *
366
 * @param Twig_Environment             $env    A Twig_Environment instance
367
 * @param Traversable|array|int|string $values The values to pick a random item from
368
 *
369
 * @throws Twig_Error_Runtime When $values is an empty array (does not apply to an empty string which is returned as is).
370
 *
371
 * @return mixed A random value from the given sequence
372
 */
373
function twig_random(Twig_Environment $env, $values = null)
374
{
375
    if (null === $values) {
376
        return mt_rand();
377
    }
378

    
379
    if (is_int($values) || is_float($values)) {
380
        return $values < 0 ? mt_rand($values, 0) : mt_rand(0, $values);
381
    }
382

    
383
    if ($values instanceof Traversable) {
384
        $values = iterator_to_array($values);
385
    } elseif (is_string($values)) {
386
        if ('' === $values) {
387
            return '';
388
        }
389
        if (null !== $charset = $env->getCharset()) {
390
            if ('UTF-8' !== $charset) {
391
                $values = twig_convert_encoding($values, 'UTF-8', $charset);
392
            }
393

    
394
            // unicode version of str_split()
395
            // split at all positions, but not after the start and not before the end
396
            $values = preg_split('/(?<!^)(?!$)/u', $values);
397

    
398
            if ('UTF-8' !== $charset) {
399
                foreach ($values as $i => $value) {
400
                    $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
401
                }
402
            }
403
        } else {
404
            return $values[mt_rand(0, strlen($values) - 1)];
405
        }
406
    }
407

    
408
    if (!is_array($values)) {
409
        return $values;
410
    }
411

    
412
    if (0 === count($values)) {
413
        throw new Twig_Error_Runtime('The random function cannot pick from an empty array.');
414
    }
415

    
416
    return $values[array_rand($values, 1)];
417
}
418

    
419
/**
420
 * Converts a date to the given format.
421
 *
422
 * <pre>
423
 *   {{ post.published_at|date("m/d/Y") }}
424
 * </pre>
425
 *
426
 * @param Twig_Environment                               $env      A Twig_Environment instance
427
 * @param DateTime|DateTimeInterface|DateInterval|string $date     A date
428
 * @param string|null                                    $format   The target format, null to use the default
429
 * @param DateTimeZone|string|null|false                 $timezone The target timezone, null to use the default, false to leave unchanged
430
 *
431
 * @return string The formatted date
432
 */
433
function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null)
434
{
435
    if (null === $format) {
436
        $formats = $env->getExtension('core')->getDateFormat();
437
        $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
438
    }
439

    
440
    if ($date instanceof DateInterval) {
441
        return $date->format($format);
442
    }
443

    
444
    return twig_date_converter($env, $date, $timezone)->format($format);
445
}
446

    
447
/**
448
 * Returns a new date object modified.
449
 *
450
 * <pre>
451
 *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
452
 * </pre>
453
 *
454
 * @param Twig_Environment $env      A Twig_Environment instance
455
 * @param DateTime|string  $date     A date
456
 * @param string           $modifier A modifier string
457
 *
458
 * @return DateTime A new date object
459
 */
460
function twig_date_modify_filter(Twig_Environment $env, $date, $modifier)
461
{
462
    $date = twig_date_converter($env, $date, false);
463
    $resultDate = $date->modify($modifier);
464

    
465
    // This is a hack to ensure PHP 5.2 support and support for DateTimeImmutable
466
    // DateTime::modify does not return the modified DateTime object < 5.3.0
467
    // and DateTimeImmutable does not modify $date.
468
    return null === $resultDate ? $date : $resultDate;
469
}
470

    
471
/**
472
 * Converts an input to a DateTime instance.
473
 *
474
 * <pre>
475
 *    {% if date(user.created_at) < date('+2days') %}
476
 *      {# do something #}
477
 *    {% endif %}
478
 * </pre>
479
 *
480
 * @param Twig_Environment                       $env      A Twig_Environment instance
481
 * @param DateTime|DateTimeInterface|string|null $date     A date
482
 * @param DateTimeZone|string|null|false         $timezone The target timezone, null to use the default, false to leave unchanged
483
 *
484
 * @return DateTime A DateTime instance
485
 */
486
function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null)
487
{
488
    // determine the timezone
489
    if (false !== $timezone) {
490
        if (null === $timezone) {
491
            $timezone = $env->getExtension('core')->getTimezone();
492
        } elseif (!$timezone instanceof DateTimeZone) {
493
            $timezone = new DateTimeZone($timezone);
494
        }
495
    }
496

    
497
    // immutable dates
498
    if ($date instanceof DateTimeImmutable) {
499
        return false !== $timezone ? $date->setTimezone($timezone) : $date;
500
    }
501

    
502
    if ($date instanceof DateTime || $date instanceof DateTimeInterface) {
503
        $date = clone $date;
504
        if (false !== $timezone) {
505
            $date->setTimezone($timezone);
506
        }
507

    
508
        return $date;
509
    }
510

    
511
    if (null === $date || 'now' === $date) {
512
        return new DateTime($date, false !== $timezone ? $timezone : $env->getExtension('core')->getTimezone());
513
    }
514

    
515
    $asString = (string) $date;
516
    if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
517
        $date = new DateTime('@'.$date);
518
    } else {
519
        $date = new DateTime($date, $env->getExtension('core')->getTimezone());
520
    }
521

    
522
    if (false !== $timezone) {
523
        $date->setTimezone($timezone);
524
    }
525

    
526
    return $date;
527
}
528

    
529
/**
530
 * Replaces strings within a string.
531
 *
532
 * @param string            $str  String to replace in
533
 * @param array|Traversable $from Replace values
534
 * @param string|null       $to   Replace to, deprecated (@see http://php.net/manual/en/function.strtr.php)
535
 *
536
 * @return string
537
 */
538
function twig_replace_filter($str, $from, $to = null)
539
{
540
    if ($from instanceof Traversable) {
541
        $from = iterator_to_array($from);
542
    } elseif (is_string($from) && is_string($to)) {
543
        @trigger_error('Using "replace" with character by character replacement is deprecated since version 1.22 and will be removed in Twig 2.0', E_USER_DEPRECATED);
544

    
545
        return strtr($str, $from, $to);
546
    } elseif (!is_array($from)) {
547
        throw new Twig_Error_Runtime(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".',is_object($from) ? get_class($from) : gettype($from)));
548
    }
549

    
550
    return strtr($str, $from);
551
}
552

    
553
/**
554
 * Rounds a number.
555
 *
556
 * @param int|float $value     The value to round
557
 * @param int|float $precision The rounding precision
558
 * @param string    $method    The method to use for rounding
559
 *
560
 * @return int|float The rounded number
561
 */
562
function twig_round($value, $precision = 0, $method = 'common')
563
{
564
    if ('common' == $method) {
565
        return round($value, $precision);
566
    }
567

    
568
    if ('ceil' != $method && 'floor' != $method) {
569
        throw new Twig_Error_Runtime('The round filter only supports the "common", "ceil", and "floor" methods.');
570
    }
571

    
572
    return $method($value * pow(10, $precision)) / pow(10, $precision);
573
}
574

    
575
/**
576
 * Number format filter.
577
 *
578
 * All of the formatting options can be left null, in that case the defaults will
579
 * be used.  Supplying any of the parameters will override the defaults set in the
580
 * environment object.
581
 *
582
 * @param Twig_Environment $env          A Twig_Environment instance
583
 * @param mixed            $number       A float/int/string of the number to format
584
 * @param int              $decimal      The number of decimal points to display.
585
 * @param string           $decimalPoint The character(s) to use for the decimal point.
586
 * @param string           $thousandSep  The character(s) to use for the thousands separator.
587
 *
588
 * @return string The formatted number
589
 */
590
function twig_number_format_filter(Twig_Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
591
{
592
    $defaults = $env->getExtension('core')->getNumberFormat();
593
    if (null === $decimal) {
594
        $decimal = $defaults[0];
595
    }
596

    
597
    if (null === $decimalPoint) {
598
        $decimalPoint = $defaults[1];
599
    }
600

    
601
    if (null === $thousandSep) {
602
        $thousandSep = $defaults[2];
603
    }
604

    
605
    return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
606
}
607

    
608
/**
609
 * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
610
 *
611
 * @param string|array $url A URL or an array of query parameters
612
 *
613
 * @return string The URL encoded value
614
 */
615
function twig_urlencode_filter($url)
616
{
617
    if (is_array($url)) {
618
        if (defined('PHP_QUERY_RFC3986')) {
619
            return http_build_query($url, '', '&', PHP_QUERY_RFC3986);
620
        }
621

    
622
        return http_build_query($url, '', '&');
623
    }
624

    
625
    return rawurlencode($url);
626
}
627

    
628
if (PHP_VERSION_ID < 50300) {
629
    /**
630
     * JSON encodes a variable.
631
     *
632
     * @param mixed $value   The value to encode.
633
     * @param int   $options Not used on PHP 5.2.x
634
     *
635
     * @return mixed The JSON encoded value
636
     */
637
    function twig_jsonencode_filter($value, $options = 0)
638
    {
639
        if ($value instanceof Twig_Markup) {
640
            $value = (string) $value;
641
        } elseif (is_array($value)) {
642
            array_walk_recursive($value, '_twig_markup2string');
643
        }
644

    
645
        return json_encode($value);
646
    }
647
} else {
648
    /**
649
     * JSON encodes a variable.
650
     *
651
     * @param mixed $value   The value to encode.
652
     * @param int   $options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT
653
     *
654
     * @return mixed The JSON encoded value
655
     */
656
    function twig_jsonencode_filter($value, $options = 0)
657
    {
658
        if ($value instanceof Twig_Markup) {
659
            $value = (string) $value;
660
        } elseif (is_array($value)) {
661
            array_walk_recursive($value, '_twig_markup2string');
662
        }
663

    
664
        return json_encode($value, $options);
665
    }
666
}
667

    
668
function _twig_markup2string(&$value)
669
{
670
    if ($value instanceof Twig_Markup) {
671
        $value = (string) $value;
672
    }
673
}
674

    
675
/**
676
 * Merges an array with another one.
677
 *
678
 * <pre>
679
 *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
680
 *
681
 *  {% set items = items|merge({ 'peugeot': 'car' }) %}
682
 *
683
 *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
684
 * </pre>
685
 *
686
 * @param array|Traversable $arr1 An array
687
 * @param array|Traversable $arr2 An array
688
 *
689
 * @return array The merged array
690
 */
691
function twig_array_merge($arr1, $arr2)
692
{
693
    if ($arr1 instanceof Traversable) {
694
        $arr1 = iterator_to_array($arr1);
695
    } elseif (!is_array($arr1)) {
696
        throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', gettype($arr1)));
697
    }
698

    
699
    if ($arr2 instanceof Traversable) {
700
        $arr2 = iterator_to_array($arr2);
701
    } elseif (!is_array($arr2)) {
702
        throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', gettype($arr2)));
703
    }
704

    
705
    return array_merge($arr1, $arr2);
706
}
707

    
708
/**
709
 * Slices a variable.
710
 *
711
 * @param Twig_Environment $env          A Twig_Environment instance
712
 * @param mixed            $item         A variable
713
 * @param int              $start        Start of the slice
714
 * @param int              $length       Size of the slice
715
 * @param bool             $preserveKeys Whether to preserve key or not (when the input is an array)
716
 *
717
 * @return mixed The sliced variable
718
 */
719
function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false)
720
{
721
    if ($item instanceof Traversable) {
722
        if ($item instanceof IteratorAggregate) {
723
            $item = $item->getIterator();
724
        }
725

    
726
        if ($start >= 0 && $length >= 0 && $item instanceof Iterator) {
727
            try {
728
                return iterator_to_array(new LimitIterator($item, $start, $length === null ? -1 : $length), $preserveKeys);
729
            } catch (OutOfBoundsException $exception) {
730
                return array();
731
            }
732
        }
733

    
734
        $item = iterator_to_array($item, $preserveKeys);
735
    }
736

    
737
    if (is_array($item)) {
738
        return array_slice($item, $start, $length, $preserveKeys);
739
    }
740

    
741
    $item = (string) $item;
742

    
743
    if (function_exists('mb_get_info') && null !== $charset = $env->getCharset()) {
744
        return (string) mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset);
745
    }
746

    
747
    return (string) (null === $length ? substr($item, $start) : substr($item, $start, $length));
748
}
749

    
750
/**
751
 * Returns the first element of the item.
752
 *
753
 * @param Twig_Environment $env  A Twig_Environment instance
754
 * @param mixed            $item A variable
755
 *
756
 * @return mixed The first element of the item
757
 */
758
function twig_first(Twig_Environment $env, $item)
759
{
760
    $elements = twig_slice($env, $item, 0, 1, false);
761

    
762
    return is_string($elements) ? $elements : current($elements);
763
}
764

    
765
/**
766
 * Returns the last element of the item.
767
 *
768
 * @param Twig_Environment $env  A Twig_Environment instance
769
 * @param mixed            $item A variable
770
 *
771
 * @return mixed The last element of the item
772
 */
773
function twig_last(Twig_Environment $env, $item)
774
{
775
    $elements = twig_slice($env, $item, -1, 1, false);
776

    
777
    return is_string($elements) ? $elements : current($elements);
778
}
779

    
780
/**
781
 * Joins the values to a string.
782
 *
783
 * The separator between elements is an empty string per default, you can define it with the optional parameter.
784
 *
785
 * <pre>
786
 *  {{ [1, 2, 3]|join('|') }}
787
 *  {# returns 1|2|3 #}
788
 *
789
 *  {{ [1, 2, 3]|join }}
790
 *  {# returns 123 #}
791
 * </pre>
792
 *
793
 * @param array  $value An array
794
 * @param string $glue  The separator
795
 *
796
 * @return string The concatenated string
797
 */
798
function twig_join_filter($value, $glue = '')
799
{
800
    if ($value instanceof Traversable) {
801
        $value = iterator_to_array($value, false);
802
    }
803

    
804
    return implode($glue, (array) $value);
805
}
806

    
807
/**
808
 * Splits the string into an array.
809
 *
810
 * <pre>
811
 *  {{ "one,two,three"|split(',') }}
812
 *  {# returns [one, two, three] #}
813
 *
814
 *  {{ "one,two,three,four,five"|split(',', 3) }}
815
 *  {# returns [one, two, "three,four,five"] #}
816
 *
817
 *  {{ "123"|split('') }}
818
 *  {# returns [1, 2, 3] #}
819
 *
820
 *  {{ "aabbcc"|split('', 2) }}
821
 *  {# returns [aa, bb, cc] #}
822
 * </pre>
823
 *
824
 * @param Twig_Environment $env       A Twig_Environment instance
825
 * @param string           $value     A string
826
 * @param string           $delimiter The delimiter
827
 * @param int              $limit     The limit
828
 *
829
 * @return array The split string as an array
830
 */
831
function twig_split_filter(Twig_Environment $env, $value, $delimiter, $limit = null)
832
{
833
    if (!empty($delimiter)) {
834
        return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
835
    }
836

    
837
    if (!function_exists('mb_get_info') || null === $charset = $env->getCharset()) {
838
        return str_split($value, null === $limit ? 1 : $limit);
839
    }
840

    
841
    if ($limit <= 1) {
842
        return preg_split('/(?<!^)(?!$)/u', $value);
843
    }
844

    
845
    $length = mb_strlen($value, $charset);
846
    if ($length < $limit) {
847
        return array($value);
848
    }
849

    
850
    $r = array();
851
    for ($i = 0; $i < $length; $i += $limit) {
852
        $r[] = mb_substr($value, $i, $limit, $charset);
853
    }
854

    
855
    return $r;
856
}
857

    
858
// The '_default' filter is used internally to avoid using the ternary operator
859
// which costs a lot for big contexts (before PHP 5.4). So, on average,
860
// a function call is cheaper.
861
/**
862
 * @internal
863
 */
864
function _twig_default_filter($value, $default = '')
865
{
866
    if (twig_test_empty($value)) {
867
        return $default;
868
    }
869

    
870
    return $value;
871
}
872

    
873
/**
874
 * Returns the keys for the given array.
875
 *
876
 * It is useful when you want to iterate over the keys of an array:
877
 *
878
 * <pre>
879
 *  {% for key in array|keys %}
880
 *      {# ... #}
881
 *  {% endfor %}
882
 * </pre>
883
 *
884
 * @param array $array An array
885
 *
886
 * @return array The keys
887
 */
888
function twig_get_array_keys_filter($array)
889
{
890
    if ($array instanceof Traversable) {
891
        return array_keys(iterator_to_array($array));
892
    }
893

    
894
    if (!is_array($array)) {
895
        return array();
896
    }
897

    
898
    return array_keys($array);
899
}
900

    
901
/**
902
 * Reverses a variable.
903
 *
904
 * @param Twig_Environment         $env          A Twig_Environment instance
905
 * @param array|Traversable|string $item         An array, a Traversable instance, or a string
906
 * @param bool                     $preserveKeys Whether to preserve key or not
907
 *
908
 * @return mixed The reversed input
909
 */
910
function twig_reverse_filter(Twig_Environment $env, $item, $preserveKeys = false)
911
{
912
    if ($item instanceof Traversable) {
913
        return array_reverse(iterator_to_array($item), $preserveKeys);
914
    }
915

    
916
    if (is_array($item)) {
917
        return array_reverse($item, $preserveKeys);
918
    }
919

    
920
    if (null !== $charset = $env->getCharset()) {
921
        $string = (string) $item;
922

    
923
        if ('UTF-8' !== $charset) {
924
            $item = twig_convert_encoding($string, 'UTF-8', $charset);
925
        }
926

    
927
        preg_match_all('/./us', $item, $matches);
928

    
929
        $string = implode('', array_reverse($matches[0]));
930

    
931
        if ('UTF-8' !== $charset) {
932
            $string = twig_convert_encoding($string, $charset, 'UTF-8');
933
        }
934

    
935
        return $string;
936
    }
937

    
938
    return strrev((string) $item);
939
}
940

    
941
/**
942
 * Sorts an array.
943
 *
944
 * @param array|Traversable $array
945
 *
946
 * @return array
947
 */
948
function twig_sort_filter($array)
949
{
950
    if ($array instanceof Traversable) {
951
        $array = iterator_to_array($array);
952
    } elseif (!is_array($array)) {
953
        throw new Twig_Error_Runtime(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', gettype($array)));
954
    }
955

    
956
    asort($array);
957

    
958
    return $array;
959
}
960

    
961
/**
962
 * @internal
963
 */
964
function twig_in_filter($value, $compare)
965
{
966
    if (is_array($compare)) {
967
        return in_array($value, $compare, is_object($value) || is_resource($value));
968
    } elseif (is_string($compare) && (is_string($value) || is_int($value) || is_float($value))) {
969
        return '' === $value || false !== strpos($compare, (string) $value);
970
    } elseif ($compare instanceof Traversable) {
971
        return in_array($value, iterator_to_array($compare, false), is_object($value) || is_resource($value));
972
    }
973

    
974
    return false;
975
}
976

    
977
/**
978
 * Escapes a string.
979
 *
980
 * @param Twig_Environment $env        A Twig_Environment instance
981
 * @param string           $string     The value to be escaped
982
 * @param string           $strategy   The escaping strategy
983
 * @param string           $charset    The charset
984
 * @param bool             $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
985
 *
986
 * @return string
987
 */
988
function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
989
{
990
    if ($autoescape && $string instanceof Twig_Markup) {
991
        return $string;
992
    }
993

    
994
    if (!is_string($string)) {
995
        if (is_object($string) && method_exists($string, '__toString')) {
996
            $string = (string) $string;
997
        } elseif (in_array($strategy, array('html', 'js', 'css', 'html_attr', 'url'))) {
998
            return $string;
999
        }
1000
    }
1001

    
1002
    if (null === $charset) {
1003
        $charset = $env->getCharset();
1004
    }
1005

    
1006
    switch ($strategy) {
1007
        case 'html':
1008
            // see http://php.net/htmlspecialchars
1009

    
1010
            // Using a static variable to avoid initializing the array
1011
            // each time the function is called. Moving the declaration on the
1012
            // top of the function slow downs other escaping strategies.
1013
            static $htmlspecialcharsCharsets;
1014

    
1015
            if (null === $htmlspecialcharsCharsets) {
1016
                if (defined('HHVM_VERSION')) {
1017
                    $htmlspecialcharsCharsets = array('utf-8' => true, 'UTF-8' => true);
1018
                } else {
1019
                    $htmlspecialcharsCharsets = array(
1020
                        'ISO-8859-1' => true, 'ISO8859-1' => true,
1021
                        'ISO-8859-15' => true, 'ISO8859-15' => true,
1022
                        'utf-8' => true, 'UTF-8' => true,
1023
                        'CP866' => true, 'IBM866' => true, '866' => true,
1024
                        'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
1025
                        '1251' => true,
1026
                        'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
1027
                        'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
1028
                        'BIG5' => true, '950' => true,
1029
                        'GB2312' => true, '936' => true,
1030
                        'BIG5-HKSCS' => true,
1031
                        'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
1032
                        'EUC-JP' => true, 'EUCJP' => true,
1033
                        'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
1034
                    );
1035
                }
1036
            }
1037

    
1038
            if (isset($htmlspecialcharsCharsets[$charset])) {
1039
                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1040
            }
1041

    
1042
            if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
1043
                // cache the lowercase variant for future iterations
1044
                $htmlspecialcharsCharsets[$charset] = true;
1045

    
1046
                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1047
            }
1048

    
1049
            $string = twig_convert_encoding($string, 'UTF-8', $charset);
1050
            $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1051

    
1052
            return twig_convert_encoding($string, $charset, 'UTF-8');
1053

    
1054
        case 'js':
1055
            // escape all non-alphanumeric characters
1056
            // into their \xHH or \uHHHH representations
1057
            if ('UTF-8' !== $charset) {
1058
                $string = twig_convert_encoding($string, 'UTF-8', $charset);
1059
            }
1060

    
1061
            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
1062
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
1063
            }
1064

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

    
1067
            if ('UTF-8' !== $charset) {
1068
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
1069
            }
1070

    
1071
            return $string;
1072

    
1073
        case 'css':
1074
            if ('UTF-8' !== $charset) {
1075
                $string = twig_convert_encoding($string, 'UTF-8', $charset);
1076
            }
1077

    
1078
            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
1079
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
1080
            }
1081

    
1082
            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string);
1083

    
1084
            if ('UTF-8' !== $charset) {
1085
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
1086
            }
1087

    
1088
            return $string;
1089

    
1090
        case 'html_attr':
1091
            if ('UTF-8' !== $charset) {
1092
                $string = twig_convert_encoding($string, 'UTF-8', $charset);
1093
            }
1094

    
1095
            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
1096
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
1097
            }
1098

    
1099
            $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string);
1100

    
1101
            if ('UTF-8' !== $charset) {
1102
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
1103
            }
1104

    
1105
            return $string;
1106

    
1107
        case 'url':
1108
            if (PHP_VERSION_ID < 50300) {
1109
                return str_replace('%7E', '~', rawurlencode($string));
1110
            }
1111

    
1112
            return rawurlencode($string);
1113

    
1114
        default:
1115
            static $escapers;
1116

    
1117
            if (null === $escapers) {
1118
                $escapers = $env->getExtension('core')->getEscapers();
1119
            }
1120

    
1121
            if (isset($escapers[$strategy])) {
1122
                return call_user_func($escapers[$strategy], $env, $string, $charset);
1123
            }
1124

    
1125
            $validStrategies = implode(', ', array_merge(array('html', 'js', 'url', 'css', 'html_attr'), array_keys($escapers)));
1126

    
1127
            throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
1128
    }
1129
}
1130

    
1131
/**
1132
 * @internal
1133
 */
1134
function twig_escape_filter_is_safe(Twig_Node $filterArgs)
1135
{
1136
    foreach ($filterArgs as $arg) {
1137
        if ($arg instanceof Twig_Node_Expression_Constant) {
1138
            return array($arg->getAttribute('value'));
1139
        }
1140

    
1141
        return array();
1142
    }
1143

    
1144
    return array('html');
1145
}
1146

    
1147
if (function_exists('mb_convert_encoding')) {
1148
    function twig_convert_encoding($string, $to, $from)
1149
    {
1150
        return mb_convert_encoding($string, $to, $from);
1151
    }
1152
} elseif (function_exists('iconv')) {
1153
    function twig_convert_encoding($string, $to, $from)
1154
    {
1155
        return iconv($from, $to, $string);
1156
    }
1157
} else {
1158
    function twig_convert_encoding($string, $to, $from)
1159
    {
1160
        throw new Twig_Error_Runtime('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
1161
    }
1162
}
1163

    
1164
function _twig_escape_js_callback($matches)
1165
{
1166
    $char = $matches[0];
1167

    
1168
    // \xHH
1169
    if (!isset($char[1])) {
1170
        return '\\x'.strtoupper(substr('00'.bin2hex($char), -2));
1171
    }
1172

    
1173
    // \uHHHH
1174
    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
1175

    
1176
    return '\\u'.strtoupper(substr('0000'.bin2hex($char), -4));
1177
}
1178

    
1179
function _twig_escape_css_callback($matches)
1180
{
1181
    $char = $matches[0];
1182

    
1183
    // \xHH
1184
    if (!isset($char[1])) {
1185
        $hex = ltrim(strtoupper(bin2hex($char)), '0');
1186
        if (0 === strlen($hex)) {
1187
            $hex = '0';
1188
        }
1189

    
1190
        return '\\'.$hex.' ';
1191
    }
1192

    
1193
    // \uHHHH
1194
    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
1195

    
1196
    return '\\'.ltrim(strtoupper(bin2hex($char)), '0').' ';
1197
}
1198

    
1199
/**
1200
 * This function is adapted from code coming from Zend Framework.
1201
 *
1202
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
1203
 * @license   http://framework.zend.com/license/new-bsd New BSD License
1204
 */
1205
function _twig_escape_html_attr_callback($matches)
1206
{
1207
    /*
1208
     * While HTML supports far more named entities, the lowest common denominator
1209
     * has become HTML5's XML Serialisation which is restricted to the those named
1210
     * entities that XML supports. Using HTML entities would result in this error:
1211
     *     XML Parsing Error: undefined entity
1212
     */
1213
    static $entityMap = array(
1214
        34 => 'quot', /* quotation mark */
1215
        38 => 'amp',  /* ampersand */
1216
        60 => 'lt',   /* less-than sign */
1217
        62 => 'gt',   /* greater-than sign */
1218
    );
1219

    
1220
    $chr = $matches[0];
1221
    $ord = ord($chr);
1222

    
1223
    /*
1224
     * The following replaces characters undefined in HTML with the
1225
     * hex entity for the Unicode replacement character.
1226
     */
1227
    if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) {
1228
        return '&#xFFFD;';
1229
    }
1230

    
1231
    /*
1232
     * Check if the current character to escape has a name entity we should
1233
     * replace it with while grabbing the hex value of the character.
1234
     */
1235
    if (strlen($chr) == 1) {
1236
        $hex = strtoupper(substr('00'.bin2hex($chr), -2));
1237
    } else {
1238
        $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8');
1239
        $hex = strtoupper(substr('0000'.bin2hex($chr), -4));
1240
    }
1241

    
1242
    $int = hexdec($hex);
1243
    if (array_key_exists($int, $entityMap)) {
1244
        return sprintf('&%s;', $entityMap[$int]);
1245
    }
1246

    
1247
    /*
1248
     * Per OWASP recommendations, we'll use hex entities for any other
1249
     * characters where a named entity does not exist.
1250
     */
1251
    return sprintf('&#x%s;', $hex);
1252
}
1253

    
1254
// add multibyte extensions if possible
1255
if (function_exists('mb_get_info')) {
1256
    /**
1257
     * Returns the length of a variable.
1258
     *
1259
     * @param Twig_Environment $env   A Twig_Environment instance
1260
     * @param mixed            $thing A variable
1261
     *
1262
     * @return int The length of the value
1263
     */
1264
    function twig_length_filter(Twig_Environment $env, $thing)
1265
    {
1266
        return is_scalar($thing) ? mb_strlen($thing, $env->getCharset()) : count($thing);
1267
    }
1268

    
1269
    /**
1270
     * Converts a string to uppercase.
1271
     *
1272
     * @param Twig_Environment $env    A Twig_Environment instance
1273
     * @param string           $string A string
1274
     *
1275
     * @return string The uppercased string
1276
     */
1277
    function twig_upper_filter(Twig_Environment $env, $string)
1278
    {
1279
        if (null !== $charset = $env->getCharset()) {
1280
            return mb_strtoupper($string, $charset);
1281
        }
1282

    
1283
        return strtoupper($string);
1284
    }
1285

    
1286
    /**
1287
     * Converts a string to lowercase.
1288
     *
1289
     * @param Twig_Environment $env    A Twig_Environment instance
1290
     * @param string           $string A string
1291
     *
1292
     * @return string The lowercased string
1293
     */
1294
    function twig_lower_filter(Twig_Environment $env, $string)
1295
    {
1296
        if (null !== $charset = $env->getCharset()) {
1297
            return mb_strtolower($string, $charset);
1298
        }
1299

    
1300
        return strtolower($string);
1301
    }
1302

    
1303
    /**
1304
     * Returns a titlecased string.
1305
     *
1306
     * @param Twig_Environment $env    A Twig_Environment instance
1307
     * @param string           $string A string
1308
     *
1309
     * @return string The titlecased string
1310
     */
1311
    function twig_title_string_filter(Twig_Environment $env, $string)
1312
    {
1313
        if (null !== $charset = $env->getCharset()) {
1314
            return mb_convert_case($string, MB_CASE_TITLE, $charset);
1315
        }
1316

    
1317
        return ucwords(strtolower($string));
1318
    }
1319

    
1320
    /**
1321
     * Returns a capitalized string.
1322
     *
1323
     * @param Twig_Environment $env    A Twig_Environment instance
1324
     * @param string           $string A string
1325
     *
1326
     * @return string The capitalized string
1327
     */
1328
    function twig_capitalize_string_filter(Twig_Environment $env, $string)
1329
    {
1330
        if (null !== $charset = $env->getCharset()) {
1331
            return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).mb_strtolower(mb_substr($string, 1, mb_strlen($string, $charset), $charset), $charset);
1332
        }
1333

    
1334
        return ucfirst(strtolower($string));
1335
    }
1336
}
1337
// and byte fallback
1338
else {
1339
    /**
1340
     * Returns the length of a variable.
1341
     *
1342
     * @param Twig_Environment $env   A Twig_Environment instance
1343
     * @param mixed            $thing A variable
1344
     *
1345
     * @return int The length of the value
1346
     */
1347
    function twig_length_filter(Twig_Environment $env, $thing)
1348
    {
1349
        return is_scalar($thing) ? strlen($thing) : count($thing);
1350
    }
1351

    
1352
    /**
1353
     * Returns a titlecased string.
1354
     *
1355
     * @param Twig_Environment $env    A Twig_Environment instance
1356
     * @param string           $string A string
1357
     *
1358
     * @return string The titlecased string
1359
     */
1360
    function twig_title_string_filter(Twig_Environment $env, $string)
1361
    {
1362
        return ucwords(strtolower($string));
1363
    }
1364

    
1365
    /**
1366
     * Returns a capitalized string.
1367
     *
1368
     * @param Twig_Environment $env    A Twig_Environment instance
1369
     * @param string           $string A string
1370
     *
1371
     * @return string The capitalized string
1372
     */
1373
    function twig_capitalize_string_filter(Twig_Environment $env, $string)
1374
    {
1375
        return ucfirst(strtolower($string));
1376
    }
1377
}
1378

    
1379
/**
1380
 * @internal
1381
 */
1382
function twig_ensure_traversable($seq)
1383
{
1384
    if ($seq instanceof Traversable || is_array($seq)) {
1385
        return $seq;
1386
    }
1387

    
1388
    return array();
1389
}
1390

    
1391
/**
1392
 * Checks if a variable is empty.
1393
 *
1394
 * <pre>
1395
 * {# evaluates to true if the foo variable is null, false, or the empty string #}
1396
 * {% if foo is empty %}
1397
 *     {# ... #}
1398
 * {% endif %}
1399
 * </pre>
1400
 *
1401
 * @param mixed $value A variable
1402
 *
1403
 * @return bool true if the value is empty, false otherwise
1404
 */
1405
function twig_test_empty($value)
1406
{
1407
    if ($value instanceof Countable) {
1408
        return 0 == count($value);
1409
    }
1410

    
1411
    return '' === $value || false === $value || null === $value || array() === $value;
1412
}
1413

    
1414
/**
1415
 * Checks if a variable is traversable.
1416
 *
1417
 * <pre>
1418
 * {# evaluates to true if the foo variable is an array or a traversable object #}
1419
 * {% if foo is traversable %}
1420
 *     {# ... #}
1421
 * {% endif %}
1422
 * </pre>
1423
 *
1424
 * @param mixed $value A variable
1425
 *
1426
 * @return bool true if the value is traversable
1427
 */
1428
function twig_test_iterable($value)
1429
{
1430
    return $value instanceof Traversable || is_array($value);
1431
}
1432

    
1433
/**
1434
 * Renders a template.
1435
 *
1436
 * @param Twig_Environment $env
1437
 * @param array            $context
1438
 * @param string|array     $template      The template to render or an array of templates to try consecutively
1439
 * @param array            $variables     The variables to pass to the template
1440
 * @param bool             $withContext
1441
 * @param bool             $ignoreMissing Whether to ignore missing templates or not
1442
 * @param bool             $sandboxed     Whether to sandbox the template or not
1443
 *
1444
 * @return string The rendered template
1445
 */
1446
function twig_include(Twig_Environment $env, $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false)
1447
{
1448
    $alreadySandboxed = false;
1449
    $sandbox = null;
1450
    if ($withContext) {
1451
        $variables = array_merge($context, $variables);
1452
    }
1453

    
1454
    if ($isSandboxed = $sandboxed && $env->hasExtension('sandbox')) {
1455
        $sandbox = $env->getExtension('sandbox');
1456
        if (!$alreadySandboxed = $sandbox->isSandboxed()) {
1457
            $sandbox->enableSandbox();
1458
        }
1459
    }
1460

    
1461
    $result = null;
1462
    try {
1463
        $result = $env->resolveTemplate($template)->render($variables);
1464
    } catch (Twig_Error_Loader $e) {
1465
        if (!$ignoreMissing) {
1466
            if ($isSandboxed && !$alreadySandboxed) {
1467
                $sandbox->disableSandbox();
1468
            }
1469

    
1470
            throw $e;
1471
        }
1472
    }
1473

    
1474
    if ($isSandboxed && !$alreadySandboxed) {
1475
        $sandbox->disableSandbox();
1476
    }
1477

    
1478
    return $result;
1479
}
1480

    
1481
/**
1482
 * Returns a template content without rendering it.
1483
 *
1484
 * @param Twig_Environment $env
1485
 * @param string           $name          The template name
1486
 * @param bool             $ignoreMissing Whether to ignore missing templates or not
1487
 *
1488
 * @return string The template source
1489
 */
1490
function twig_source(Twig_Environment $env, $name, $ignoreMissing = false)
1491
{
1492
    try {
1493
        return $env->getLoader()->getSource($name);
1494
    } catch (Twig_Error_Loader $e) {
1495
        if (!$ignoreMissing) {
1496
            throw $e;
1497
        }
1498
    }
1499
}
1500

    
1501
/**
1502
 * Provides the ability to get constants from instances as well as class/global constants.
1503
 *
1504
 * @param string      $constant The name of the constant
1505
 * @param null|object $object   The object to get the constant from
1506
 *
1507
 * @return string
1508
 */
1509
function twig_constant($constant, $object = null)
1510
{
1511
    if (null !== $object) {
1512
        $constant = get_class($object).'::'.$constant;
1513
    }
1514

    
1515
    return constant($constant);
1516
}
1517

    
1518
/**
1519
 * Batches item.
1520
 *
1521
 * @param array $items An array of items
1522
 * @param int   $size  The size of the batch
1523
 * @param mixed $fill  A value used to fill missing items
1524
 *
1525
 * @return array
1526
 */
1527
function twig_array_batch($items, $size, $fill = null)
1528
{
1529
    if ($items instanceof Traversable) {
1530
        $items = iterator_to_array($items, false);
1531
    }
1532

    
1533
    $size = ceil($size);
1534

    
1535
    $result = array_chunk($items, $size, true);
1536

    
1537
    if (null !== $fill && !empty($result)) {
1538
        $last = count($result) - 1;
1539
        if ($fillCount = $size - count($result[$last])) {
1540
            $result[$last] = array_merge(
1541
                $result[$last],
1542
                array_fill(0, $fillCount, $fill)
1543
            );
1544
        }
1545
    }
1546

    
1547
    return $result;
1548
}
(1-1/10)