Passed
Push — master ( 020752...f1625c )
by David
05:47 queued 02:32
created

lib/Dwoo/Compiler.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Copyright (c) 2013-2017
4
 *
5
 * @category  Library
6
 * @package   Dwoo
7
 * @author    Jordi Boggiano <[email protected]>
8
 * @author    David Sanchez <[email protected]>
9
 * @copyright 2008-2013 Jordi Boggiano
10
 * @copyright 2013-2017 David Sanchez
11
 * @license   http://dwoo.org/LICENSE LGPLv3
12
 * @version   1.3.6
13
 * @date      2017-03-22
14
 * @link      http://dwoo.org/
15
 */
16
17
namespace Dwoo;
18
19
use Closure;
20
use Dwoo\Plugins\Blocks\PluginIf;
21
use Dwoo\Security\Exception as SecurityException;
22
use Dwoo\Security\Policy as SecurityPolicy;
23
use Dwoo\Compilation\Exception as CompilationException;
24
use ReflectionFunction;
25
use ReflectionMethod;
26
27
/**
28
 * Default dwoo compiler class, compiles dwoo templates into php.
29
 * This software is provided 'as-is', without any express or implied warranty.
30
 * In no event will the authors be held liable for any damages arising from the use of this software.
31
 */
32
class Compiler implements ICompiler
33
{
34
    /**
35
     * Constant that represents a php opening tag.
36
     * use it in case it needs to be adjusted
37
     *
38
     * @var string
39
     */
40
    const PHP_OPEN = '<?php ';
41
42
    /**
43
     * Constant that represents a php closing tag.
44
     * use it in case it needs to be adjusted
45
     *
46
     * @var string
47
     */
48
    const PHP_CLOSE = '?>';
49
50
    /**
51
     * Boolean flag to enable or disable debugging output.
52
     *
53
     * @var bool
54
     */
55
    public $debug = false;
56
57
    /**
58
     * Left script delimiter.
59
     *
60
     * @var string
61
     */
62
    protected $ld = '{';
63
64
    /**
65
     * Left script delimiter with escaped regex meta characters.
66
     *
67
     * @var string
68
     */
69
    protected $ldr = '\\{';
70
71
    /**
72
     * Right script delimiter.
73
     *
74
     * @var string
75
     */
76
    protected $rd = '}';
77
78
    /**
79
     * Right script delimiter with escaped regex meta characters.
80
     *
81
     * @var string
82
     */
83
    protected $rdr = '\\}';
84
85
    /**
86
     * Defines whether the nested comments should be parsed as nested or not.
87
     * defaults to false (classic block comment parsing as in all languages)
88
     *
89
     * @var bool
90
     */
91
    protected $allowNestedComments = false;
92
93
    /**
94
     * Defines whether opening and closing tags can contain spaces before valid data or not.
95
     * turn to true if you want to be sloppy with the syntax, but when set to false it allows
96
     * to skip javascript and css tags as long as they are in the form "{ something", which is
97
     * nice. default is false.
98
     *
99
     * @var bool
100
     */
101
    protected $allowLooseOpenings = false;
102
103
    /**
104
     * Defines whether the compiler will automatically html-escape variables or not.
105
     * default is false
106
     *
107
     * @var bool
108
     */
109
    protected $autoEscape = false;
110
111
    /**
112
     * Security policy object.
113
     *
114
     * @var SecurityPolicy
115
     */
116
    protected $securityPolicy;
117
118
    /**
119
     * Stores the custom plugins registered with this compiler.
120
     *
121
     * @var array
122
     */
123
    protected $customPlugins = array();
124
125
    /**
126
     * Stores the template plugins registered with this compiler.
127
     *
128
     * @var array
129
     */
130
    protected $templatePlugins = array();
131
132
    /**
133
     * Stores the pre- and post-processors callbacks.
134
     *
135
     * @var array
136
     */
137
    protected $processors = array('pre' => array(), 'post' => array());
138
139
    /**
140
     * Stores a list of plugins that are used in the currently compiled
141
     * template, and that are not compilable. these plugins will be loaded
142
     * during the template's runtime if required.
143
     * it is a 1D array formatted as key:pluginName value:pluginType
144
     *
145
     * @var array
146
     */
147
    protected $usedPlugins;
148
149
    /**
150
     * Stores the template undergoing compilation.
151
     *
152
     * @var string
153
     */
154
    protected $template;
155
156
    /**
157
     * Stores the current pointer position inside the template.
158
     *
159
     * @var int
160
     */
161
    protected $pointer;
162
163
    /**
164
     * Stores the current line count inside the template for debugging purposes.
165
     *
166
     * @var int
167
     */
168
    protected $line;
169
170
    /**
171
     * Stores the current template source while compiling it.
172
     *
173
     * @var string
174
     */
175
    protected $templateSource;
176
177
    /**
178
     * Stores the data within which the scope moves.
179
     *
180
     * @var array
181
     */
182
    protected $data;
183
184
    /**
185
     * Variable scope of the compiler, set to null if
186
     * it can not be resolved to a static string (i.e. if some
187
     * plugin defines a new scope based on a variable array key).
188
     *
189
     * @var mixed
190
     */
191
    protected $scope;
192
193
    /**
194
     * Variable scope tree, that allows to rebuild the current
195
     * scope if required, i.e. when going to a parent level.
196
     *
197
     * @var array
198
     */
199
    protected $scopeTree;
200
201
    /**
202
     * Block plugins stack, accessible through some methods.
203
     *
204
     * @see findBlock
205
     * @see getCurrentBlock
206
     * @see addBlock
207
     * @see addCustomBlock
208
     * @see injectBlock
209
     * @see removeBlock
210
     * @see removeTopBlock
211
     * @var array
212
     */
213
    protected $stack = array();
214
215
    /**
216
     * Current block at the top of the block plugins stack,
217
     * accessible through getCurrentBlock.
218
     *
219
     * @see getCurrentBlock
220
     * @var array
221
     */
222
    protected $curBlock;
223
224
    /**
225
     * Current dwoo object that uses this compiler, or null.
226
     *
227
     * @var Core
228
     */
229
    public $core;
230
231
    /**
232
     * Holds an instance of this class, used by getInstance when you don't
233
     * provide a custom compiler in order to save resources.
234
     *
235
     * @var Compiler
236
     */
237
    protected static $instance;
238
239
    /**
240
     * Token types.
241
     *
242
     * @var int
243
     */
244
    const T_UNQUOTED_STRING = 1;
245
    const T_NUMERIC         = 2;
246
    const T_NULL            = 4;
247
    const T_BOOL            = 8;
248
    const T_MATH            = 16;
249
    const T_BREAKCHAR       = 32;
250
251
    /**
252
     * Compiler constructor.
253
     * saves the created instance so that child templates get the same one
254
     */
255
    public function __construct()
256
    {
257
        self::$instance = $this;
258
    }
259
260
    /**
261
     * Sets the delimiters to use in the templates.
262
     * delimiters can be multi-character strings but should not be one of those as they will
263
     * make it very hard to work with templates or might even break the compiler entirely : "\", "$", "|", ":" and
264
     * finally "#" only if you intend to use config-vars with the #var# syntax.
265
     *
266
     * @param string $left  left delimiter
267
     * @param string $right right delimiter
268
     */
269
    public function setDelimiters($left, $right)
270
    {
271
        $this->ld  = $left;
272
        $this->rd  = $right;
273
        $this->ldr = preg_quote($left, '/');
274
        $this->rdr = preg_quote($right, '/');
275
    }
276
277
    /**
278
     * Returns the left and right template delimiters.
279
     *
280
     * @return array containing the left and the right delimiters
281
     */
282
    public function getDelimiters()
283
    {
284
        return array($this->ld, $this->rd);
285
    }
286
287
    /**
288
     * Sets the way to handle nested comments, if set to true
289
     * {* foo {* some other *} comment *} will be stripped correctly.
290
     * if false it will remove {* foo {* some other *} and leave "comment *}" alone,
291
     * this is the default behavior
292
     *
293
     * @param bool $allow allow nested comments or not, defaults to true (but the default internal value is false)
294
     */
295
    public function setNestedCommentsHandling($allow = true)
296
    {
297
        $this->allowNestedComments = (bool)$allow;
298
    }
299
300
    /**
301
     * Returns the nested comments handling setting.
302
     *
303
     * @see    setNestedCommentsHandling
304
     * @return bool true if nested comments are allowed
305
     */
306
    public function getNestedCommentsHandling()
307
    {
308
        return $this->allowNestedComments;
309
    }
310
311
    /**
312
     * Sets the tag openings handling strictness, if set to true, template tags can
313
     * contain spaces before the first function/string/variable such as { $foo} is valid.
314
     * if set to false (default setting), { $foo} is invalid but that is however a good thing
315
     * as it allows css (i.e. #foo { color:red; }) to be parsed silently without triggering
316
     * an error, same goes for javascript.
317
     *
318
     * @param bool $allow true to allow loose handling, false to restore default setting
319
     */
320
    public function setLooseOpeningHandling($allow = false)
321
    {
322
        $this->allowLooseOpenings = (bool)$allow;
323
    }
324
325
    /**
326
     * Returns the tag openings handling strictness setting.
327
     *
328
     * @see    setLooseOpeningHandling
329
     * @return bool true if loose tags are allowed
330
     */
331
    public function getLooseOpeningHandling()
332
    {
333
        return $this->allowLooseOpenings;
334
    }
335
336
    /**
337
     * Changes the auto escape setting.
338
     * if enabled, the compiler will automatically html-escape variables,
339
     * unless they are passed through the safe function such as {$var|safe}
340
     * or {safe $var}
341
     * default setting is disabled/false
342
     *
343
     * @param bool $enabled set to true to enable, false to disable
344
     */
345
    public function setAutoEscape($enabled)
346
    {
347
        $this->autoEscape = (bool)$enabled;
348
    }
349
350
    /**
351
     * Returns the auto escape setting.
352
     * default setting is disabled/false
353
     *
354
     * @return bool
355
     */
356
    public function getAutoEscape()
357
    {
358
        return $this->autoEscape;
359
    }
360
361
    /**
362
     * Adds a preprocessor to the compiler, it will be called
363
     * before the template is compiled.
364
     *
365
     * @param mixed $callback either a valid callback to the preprocessor or a simple name if the autoload is set to
366
     *                        true
367
     * @param bool  $autoload if set to true, the preprocessor is auto-loaded from one of the plugin directories, else
368
     *                        you must provide a valid callback
369
     */
370 View Code Duplication
    public function addPreProcessor($callback, $autoload = false)
371
    {
372
        if ($autoload) {
373
            $name  = str_replace(Core::NAMESPACE_PLUGINS_PROCESSORS, '', Core::toCamelCase($callback));
374
            $class = Core::NAMESPACE_PLUGINS_PROCESSORS . $name;
375
376
            if (class_exists($class)) {
377
                $callback = array(new $class($this), 'process');
378
            } elseif (function_exists($class)) {
379
                $callback = $class;
380
            } else {
381
                $callback = array('autoload' => true, 'class' => $class, 'name' => $name);
382
            }
383
384
            $this->processors['pre'][] = $callback;
385
        } else {
386
            $this->processors['pre'][] = $callback;
387
        }
388
    }
389
390
    /**
391
     * Removes a preprocessor from the compiler.
392
     *
393
     * @param mixed $callback either a valid callback to the preprocessor or a simple name if it was autoloaded
394
     */
395 View Code Duplication
    public function removePreProcessor($callback)
396
    {
397
        if (($index = array_search($callback, $this->processors['pre'], true)) !== false) {
398
            unset($this->processors['pre'][$index]);
399
        } elseif (($index = array_search(Core::NAMESPACE_PLUGINS_PROCESSORS . str_replace(Core::NAMESPACE_PLUGINS_PROCESSORS, '',
400
                    $callback),
401
                $this->processors['pre'], true)) !== false) {
402
            unset($this->processors['pre'][$index]);
403
        } else {
404
            $class = Core::NAMESPACE_PLUGINS_PROCESSORS . str_replace(Core::NAMESPACE_PLUGINS_PROCESSORS, '', $callback);
405
            foreach ($this->processors['pre'] as $index => $proc) {
406
                if (is_array($proc) && ($proc[0] instanceof $class) || (isset($proc['class']) && $proc['class'] == $class)) {
407
                    unset($this->processors['pre'][$index]);
408
                    break;
409
                }
410
            }
411
        }
412
    }
413
414
    /**
415
     * Adds a postprocessor to the compiler, it will be called
416
     * before the template is compiled.
417
     *
418
     * @param mixed $callback either a valid callback to the postprocessor or a simple name if the autoload is set to
419
     *                        true
420
     * @param bool  $autoload if set to true, the postprocessor is auto-loaded from one of the plugin directories, else
421
     *                        you must provide a valid callback
422
     */
423 View Code Duplication
    public function addPostProcessor($callback, $autoload = false)
424
    {
425
        if ($autoload) {
426
            $name  = str_replace(Core::NAMESPACE_PLUGINS_PROCESSORS, '', $callback);
427
            $class = Core::NAMESPACE_PLUGINS_PROCESSORS . Core::toCamelCase($name);
428
429
            if (class_exists($class)) {
430
                $callback = array(new $class($this), 'process');
431
            } elseif (function_exists($class)) {
432
                $callback = $class;
433
            } else {
434
                $callback = array('autoload' => true, 'class' => $class, 'name' => $name);
435
            }
436
437
            $this->processors['post'][] = $callback;
438
        } else {
439
            $this->processors['post'][] = $callback;
440
        }
441
    }
442
443
    /**
444
     * Removes a postprocessor from the compiler.
445
     *
446
     * @param mixed $callback either a valid callback to the postprocessor or a simple name if it was autoloaded
447
     */
448 View Code Duplication
    public function removePostProcessor($callback)
449
    {
450
        if (($index = array_search($callback, $this->processors['post'], true)) !== false) {
451
            unset($this->processors['post'][$index]);
452
        } elseif (($index = array_search(Core::NAMESPACE_PLUGINS_PROCESSORS . str_replace(Core::NAMESPACE_PLUGINS_PROCESSORS, '',
453
                    $callback),
454
                $this->processors['post'], true)) !== false) {
455
            unset($this->processors['post'][$index]);
456
        } else {
457
            $class = Core::NAMESPACE_PLUGINS_PROCESSORS . str_replace(Core::NAMESPACE_PLUGINS_PROCESSORS, '', $callback);
458
            foreach ($this->processors['post'] as $index => $proc) {
459
                if (is_array($proc) && ($proc[0] instanceof $class) || (isset($proc['class']) && $proc['class'] == $class)) {
460
                    unset($this->processors['post'][$index]);
461
                    break;
462
                }
463
            }
464
        }
465
    }
466
467
    /**
468
     * Internal function to autoload processors at runtime if required.
469
     *
470
     * @param string $class the class/function name
471
     * @param string $name  the plugin name (without Dwoo_Plugin_ prefix)
472
     *
473
     * @return array|string
474
     * @throws Exception
475
     */
476
    protected function loadProcessor($class, $name)
477
    {
478
        if (!class_exists($class) && !function_exists($class)) {
479
            try {
480
                $this->getCore()->getLoader()->loadPlugin($name);
481
            }
482
            catch (Exception $e) {
483
                throw new Exception('Processor ' . $name . ' could not be found in your plugin directories, please ensure it is in a file named ' . $name . '.php in the plugin directory');
484
            }
485
        }
486
487
        if (class_exists($class)) {
488
            return array(new $class($this), 'process');
489
        }
490
491
        if (function_exists($class)) {
492
            return $class;
493
        }
494
495
        throw new Exception('Wrong processor name, when using autoload the processor must be in one of your plugin dir as "name.php" containg a class or function named "Dwoo_Processor_name"');
496
    }
497
498
    /**
499
     * Adds an used plugin, this is reserved for use by the {template} plugin.
500
     * this is required so that plugin loading bubbles up from loaded
501
     * template files to the current one
502
     *
503
     * @private
504
     *
505
     * @param string $name function name
506
     * @param int    $type plugin type (Core::*_PLUGIN)
507
     */
508
    public function addUsedPlugin($name, $type)
509
    {
510
        $this->usedPlugins[$name] = $type;
511
    }
512
513
    /**
514
     * Returns all the plugins this template uses.
515
     *
516
     * @private
517
     * @return  array the list of used plugins in the parsed template
518
     */
519
    public function getUsedPlugins()
520
    {
521
        return $this->usedPlugins;
522
    }
523
524
    /**
525
     * Adds a template plugin, this is reserved for use by the {template} plugin.
526
     * this is required because the template functions are not declared yet
527
     * during compilation, so we must have a way of validating their argument
528
     * signature without using the reflection api
529
     *
530
     * @private
531
     *
532
     * @param string $name   function name
533
     * @param array  $params parameter array to help validate the function call
534
     * @param string $uuid   unique id of the function
535
     * @param string $body   function php code
536
     */
537
    public function addTemplatePlugin($name, array $params, $uuid, $body = null)
538
    {
539
        $this->templatePlugins[$name] = array('params' => $params, 'body' => $body, 'uuid' => $uuid);
540
    }
541
542
    /**
543
     * Returns all the parsed sub-templates.
544
     *
545
     * @private
546
     * @return  array the parsed sub-templates
547
     */
548
    public function getTemplatePlugins()
549
    {
550
        return $this->templatePlugins;
551
    }
552
553
    /**
554
     * Marks a template plugin as being called, which means its source must be included in the compiled template.
555
     *
556
     * @param string $name function name
557
     */
558
    public function useTemplatePlugin($name)
559
    {
560
        $this->templatePlugins[$name]['called'] = true;
561
    }
562
563
    /**
564
     * Adds the custom plugins loaded into Dwoo to the compiler so it can load them.
565
     *
566
     * @see Core::addPlugin
567
     *
568
     * @param array $customPlugins an array of custom plugins
569
     */
570
    public function setCustomPlugins(array $customPlugins)
571
    {
572
        $this->customPlugins = $customPlugins;
573
    }
574
575
    /**
576
     * Sets the security policy object to enforce some php security settings.
577
     * use this if untrusted persons can modify templates,
578
     * set it on the Dwoo object as it will be passed onto the compiler automatically
579
     *
580
     * @param SecurityPolicy $policy the security policy object
581
     */
582
    public function setSecurityPolicy(SecurityPolicy $policy = null)
583
    {
584
        $this->securityPolicy = $policy;
585
    }
586
587
    /**
588
     * Returns the current security policy object or null by default.
589
     *
590
     * @return SecurityPolicy|null the security policy object if any
591
     */
592
    public function getSecurityPolicy()
593
    {
594
        return $this->securityPolicy;
595
    }
596
597
    /**
598
     * Sets the pointer position.
599
     *
600
     * @param int  $position the new pointer position
601
     * @param bool $isOffset if set to true, the position acts as an offset and not an absolute position
602
     */
603
    public function setPointer($position, $isOffset = false)
604
    {
605
        if ($isOffset) {
606
            $this->pointer += $position;
607
        } else {
608
            $this->pointer = $position;
609
        }
610
    }
611
612
    /**
613
     * Returns the current pointer position, only available during compilation of a template.
614
     *
615
     * @return int
616
     */
617
    public function getPointer()
618
    {
619
        return $this->pointer;
620
    }
621
622
    /**
623
     * Sets the line number.
624
     *
625
     * @param int  $number   the new line number
626
     * @param bool $isOffset if set to true, the position acts as an offset and not an absolute position
627
     */
628
    public function setLine($number, $isOffset = false)
629
    {
630
        if ($isOffset) {
631
            $this->line += $number;
632
        } else {
633
            $this->line = $number;
634
        }
635
    }
636
637
    /**
638
     * Returns the current line number, only available during compilation of a template.
639
     *
640
     * @return int
641
     */
642
    public function getLine()
643
    {
644
        return $this->line;
645
    }
646
647
    /**
648
     * Returns the dwoo object that initiated this template compilation, only available during compilation of a
649
     * template.
650
     *
651
     * @return Core
652
     */
653
    public function getCore()
654
    {
655
        return $this->core;
656
    }
657
658
    /**
659
     * Overwrites the template that is being compiled.
660
     *
661
     * @param string $newSource   the template source that must replace the current one
662
     * @param bool   $fromPointer if set to true, only the source from the current pointer position is replaced
663
     *
664
     * @return void
665
     */
666
    public function setTemplateSource($newSource, $fromPointer = false)
667
    {
668
        if ($fromPointer === true) {
669
            $this->templateSource = substr($this->templateSource, 0, $this->pointer) . $newSource;
670
        } else {
671
            $this->templateSource = $newSource;
672
        }
673
    }
674
675
    /**
676
     * Returns the template that is being compiled.
677
     *
678
     * @param mixed $fromPointer if set to true, only the source from the current pointer
679
     *                           position is returned, if a number is given it overrides the current pointer
680
     *
681
     * @return string the template or partial template
682
     */
683
    public function getTemplateSource($fromPointer = false)
684
    {
685
        if ($fromPointer === true) {
686
            return substr($this->templateSource, $this->pointer);
687
        } elseif (is_numeric($fromPointer)) {
688
            return substr($this->templateSource, $fromPointer);
689
        } else {
690
            return $this->templateSource;
691
        }
692
    }
693
694
    /**
695
     * Resets the compilation pointer, effectively restarting the compilation process.
696
     * this is useful if a plugin modifies the template source since it might need to be recompiled
697
     */
698
    public function recompile()
699
    {
700
        $this->setPointer(0);
701
    }
702
703
    /**
704
     * Compiles the provided string down to php code.
705
     *
706
     * @param Core      $core
707
     * @param ITemplate $template the template to compile
708
     *
709
     * @return string a compiled php string
710
     * @throws CompilationException
711
     */
712
    public function compile(Core $core, ITemplate $template)
713
    {
714
        // init vars
715
        //		$compiled = '';
716
        $tpl                  = $template->getSource();
717
        $ptr                  = 0;
718
        $this->core           = $core;
719
        $this->template       = $template;
720
        $this->templateSource = &$tpl;
721
        $this->pointer        = &$ptr;
722
723
        while (true) {
724
            // if pointer is at the beginning, reset everything, that allows a plugin to externally reset the compiler if everything must be reparsed
725
            if ($ptr === 0) {
726
                // resets variables
727
                $this->usedPlugins     = array();
728
                $this->data            = array();
729
                $this->scope           = &$this->data;
730
                $this->scopeTree       = array();
731
                $this->stack           = array();
732
                $this->line            = 1;
733
                $this->templatePlugins = array();
734
                // add top level block
735
                $compiled                 = $this->addBlock('TopLevelBlock', array(), 0);
736
                $this->stack[0]['buffer'] = '';
737
738
                if ($this->debug) {
739
                    echo "\n";
740
                    echo 'COMPILER INIT' . "\n";
741
                }
742
743
                if ($this->debug) {
744
                    echo 'PROCESSING PREPROCESSORS (' . count($this->processors['pre']) . ')' . "\n";
745
                }
746
747
                // runs preprocessors
748 View Code Duplication
                foreach ($this->processors['pre'] as $preProc) {
749
                    if (is_array($preProc) && isset($preProc['autoload'])) {
750
                        $preProc = $this->loadProcessor($preProc['class'], $preProc['name']);
751
                    }
752
                    if (is_array($preProc) && $preProc[0] instanceof Processor) {
753
                        $tpl = call_user_func($preProc, $tpl);
754
                    } else {
755
                        $tpl = call_user_func($preProc, $this, $tpl);
756
                    }
757
                }
758
                unset($preProc);
759
760
                // show template source if debug
761
                if ($this->debug) {
762
                    echo '<pre>'.print_r(htmlentities($tpl), true).'</pre>'."\n";
763
                }
764
765
                // strips php tags if required by the security policy
766
                if ($this->securityPolicy !== null) {
767
                    $search = array('{<\?php.*?\?>}');
768
                    if (ini_get('short_open_tags')) {
769
                        $search = array('{<\?.*?\?>}', '{<%.*?%>}');
770
                    }
771
                    switch ($this->securityPolicy->getPhpHandling()) {
772
                        case SecurityPolicy::PHP_ALLOW:
773
                            break;
774
                        case SecurityPolicy::PHP_ENCODE:
775
                            $tpl = preg_replace_callback($search, array($this, 'phpTagEncodingHelper'), $tpl);
776
                            break;
777
                        case SecurityPolicy::PHP_REMOVE:
778
                            $tpl = preg_replace($search, '', $tpl);
779
                    }
780
                }
781
            }
782
783
            $pos = strpos($tpl, $this->ld, $ptr);
784
785
            if ($pos === false) {
786
                $this->push(substr($tpl, $ptr), 0);
787
                break;
788
            } elseif (substr($tpl, $pos - 1, 1) === '\\' && substr($tpl, $pos - 2, 1) !== '\\') {
789
                $this->push(substr($tpl, $ptr, $pos - $ptr - 1) . $this->ld);
790
                $ptr = $pos + strlen($this->ld);
791
            } elseif (preg_match('/^' . $this->ldr . ($this->allowLooseOpenings ? '\s*' : '') . 'literal' . ($this->allowLooseOpenings ? '\s*' : '') . $this->rdr . '/s', substr($tpl, $pos), $litOpen)) {
792
                if (!preg_match('/' . $this->ldr . ($this->allowLooseOpenings ? '\s*' : '') . '\/literal' . ($this->allowLooseOpenings ? '\s*' : '') . $this->rdr . '/s', $tpl, $litClose, PREG_OFFSET_CAPTURE, $pos)) {
793
                    throw new CompilationException($this, 'The {literal} blocks must be closed explicitly with {/literal}');
794
                }
795
                $endpos = $litClose[0][1];
796
                $this->push(substr($tpl, $ptr, $pos - $ptr) . substr($tpl, $pos + strlen($litOpen[0]), $endpos - $pos - strlen($litOpen[0])));
797
                $ptr = $endpos + strlen($litClose[0][0]);
798
            } else {
799
                if (substr($tpl, $pos - 2, 1) === '\\' && substr($tpl, $pos - 1, 1) === '\\') {
800
                    $this->push(substr($tpl, $ptr, $pos - $ptr - 1));
801
                    $ptr = $pos;
802
                }
803
804
                $this->push(substr($tpl, $ptr, $pos - $ptr));
805
                $ptr = $pos;
806
807
                $pos += strlen($this->ld);
808
                if ($this->allowLooseOpenings) {
809
                    while (substr($tpl, $pos, 1) === ' ') {
810
                        $pos += 1;
811
                    }
812
                } else {
813
                    if (substr($tpl, $pos, 1) === ' ' || substr($tpl, $pos, 1) === "\r" || substr($tpl, $pos, 1) === "\n" || substr($tpl, $pos, 1) === "\t") {
814
                        $ptr = $pos;
815
                        $this->push($this->ld);
816
                        continue;
817
                    }
818
                }
819
820
                // check that there is an end tag present
821 View Code Duplication
                if (strpos($tpl, $this->rd, $pos) === false) {
822
                    throw new CompilationException($this, 'A template tag was not closed, started with "' . substr($tpl, $ptr, 30) . '"');
823
                }
824
825
                $ptr += strlen($this->ld);
826
                $subptr = $ptr;
827
828
                while (true) {
829
                    $parsed = $this->parse($tpl, $subptr, null, false, 'root', $subptr);
830
831
                    // reload loop if the compiler was reset
832
                    if ($ptr === 0) {
833
                        continue 2;
834
                    }
835
836
                    $len = $subptr - $ptr;
837
                    $this->push($parsed, substr_count(substr($tpl, $ptr, $len), "\n"));
838
                    $ptr += $len;
839
840
                    if ($parsed === false) {
841
                        break;
842
                    }
843
                }
844
            }
845
        }
846
847
        $compiled .= $this->removeBlock('TopLevelBlock');
848
849
        if ($this->debug) {
850
            echo 'PROCESSING POSTPROCESSORS' . "\n";
851
        }
852
853 View Code Duplication
        foreach ($this->processors['post'] as $postProc) {
854
            if (is_array($postProc) && isset($postProc['autoload'])) {
855
                $postProc = $this->loadProcessor($postProc['class'], $postProc['name']);
856
            }
857
            if (is_array($postProc) && $postProc[0] instanceof Processor) {
858
                $compiled = call_user_func($postProc, $compiled);
859
            } else {
860
                $compiled = call_user_func($postProc, $this, $compiled);
861
            }
862
        }
863
        unset($postProc);
864
865
        if ($this->debug) {
866
            echo 'COMPILATION COMPLETE : MEM USAGE : ' . memory_get_usage() . "\n";
867
        }
868
869
        $output = "<?php\n/* template head */\n";
870
871
        // build plugin preloader
872
        foreach ($this->getUsedPlugins() as $plugin => $type) {
873
            if ($type & Core::CUSTOM_PLUGIN) {
874
                continue;
875
            }
876
877
            switch ($type) {
878
                case Core::CLASS_PLUGIN:
879 View Code Duplication
                case Core::CLASS_PLUGIN + Core::BLOCK_PLUGIN:
880
                    if (class_exists('Plugin' . $plugin) !== false) {
881
                        $output .= "if (class_exists('" . "Plugin" . $plugin . "')===false)".
882
                        "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
883
                    } else {
884
                        $output .= "if (class_exists('" . Core::NAMESPACE_PLUGINS_BLOCKS . "Plugin" . $plugin . "')===false)".
885
                        "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
886
                    }
887
                    break;
888 View Code Duplication
                case Core::CLASS_PLUGIN + Core::FUNC_PLUGIN:
889
                    if (class_exists('Plugin' . $plugin) !== false) {
890
                        $output .= "if (class_exists('" . "Plugin" . $plugin . "')===false)".
891
                            "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
892
                    } else {
893
                        $output .= "if (class_exists('" . Core::NAMESPACE_PLUGINS_FUNCTIONS . "Plugin" . $plugin . "')===false)".
894
                            "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
895
                    }
896
                    break;
897 View Code Duplication
                case Core::FUNC_PLUGIN:
898
                    if (function_exists('Plugin' . $plugin) !== false) {
899
                        $output .= "if (function_exists('" . "Plugin" . $plugin . "')===false)".
900
                        "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
901
                    } else {
902
                        $output .= "if (function_exists('" . Core::NAMESPACE_PLUGINS_FUNCTIONS . "Plugin" . $plugin . "')===false)".
903
                        "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
904
                    }
905
                    break;
906
                case Core::SMARTY_MODIFIER:
907
                    $output .= "if (function_exists('smarty_modifier_$plugin')===false)".
908
                    "\n\t\$this->getLoader()->loadPlugin('$plugin');\n";
909
                    break;
910
                case Core::SMARTY_FUNCTION:
911
                    $output .= "if (function_exists('smarty_function_$plugin')===false)".
912
                    "\n\t\$this->getLoader()->loadPlugin('$plugin');\n";
913
                    break;
914
                case Core::SMARTY_BLOCK:
915
                    $output .= "if (function_exists('smarty_block_$plugin')===false)".
916
                    "\n\t\$this->getLoader()->loadPlugin('$plugin');\n";
917
                    break;
918
                case Core::PROXY_PLUGIN:
919
                    $output .= $this->getCore()->getPluginProxy()->getLoader($plugin);
920
                    break;
921
                default:
922
                    throw new CompilationException($this, 'Type error for ' . $plugin . ' with type' . $type);
923
            }
924
        }
925
926
        foreach ($this->templatePlugins as $function => $attr) {
927
            if (isset($attr['called']) && $attr['called'] === true && !isset($attr['checked'])) {
928
                $this->resolveSubTemplateDependencies($function);
929
            }
930
        }
931
        foreach ($this->templatePlugins as $function) {
932
            if (isset($function['called']) && $function['called'] === true) {
933
                $output .= $function['body'] . PHP_EOL;
934
            }
935
        }
936
937
        $output .= $compiled . "\n?>";
938
939
        $output = preg_replace('/(?<!;|\}|\*\/|\n|\{)(\s*' . preg_quote(self::PHP_CLOSE, '/') . preg_quote(self::PHP_OPEN, '/') . ')/', ";\n", $output);
940
        $output = str_replace(self::PHP_CLOSE . self::PHP_OPEN, "\n", $output);
941
942
        // handle <?xml tag at the beginning
943
        $output = preg_replace('#(/\* template body \*/ \?>\s*)<\?xml#is', '$1<?php echo \'<?xml\'; ?>', $output);
944
945
        // add another line break after PHP closing tags that have a line break following,
946
        // as we do not know whether it's intended, and PHP will strip it otherwise
947
        $output = preg_replace('/(?<!"|<\?xml)\s*\?>\n/', '$0' . "\n", $output);
948
949
        if ($this->debug) {
950
            echo '=============================================================================================' . "\n";
951
            $lines = preg_split('{\r\n|\n|<br />}', $output);
952
            array_shift($lines);
953
            foreach ($lines as $i => $line) {
954
                echo ($i + 1) . '. ' . $line . "\r\n";
955
            }
956
            echo '=============================================================================================' . "\n";
957
        }
958
959
        $this->template = $this->dwoo = null;
960
        $tpl            = null;
961
962
        return $output;
963
    }
964
965
    /**
966
     * Checks what sub-templates are used in every sub-template so that we're sure they are all compiled.
967
     *
968
     * @param string $function the sub-template name
969
     */
970
    protected function resolveSubTemplateDependencies($function)
971
    {
972
        if ($this->debug) {
973
            echo 'Compiler::' . __FUNCTION__ . "\n";
974
        }
975
976
        $body = $this->templatePlugins[$function]['body'];
977
        foreach ($this->templatePlugins as $func => $attr) {
978
            if ($func !== $function && !isset($attr['called']) && strpos($body, Core::NAMESPACE_PLUGINS_FUNCTIONS .
979
            'Plugin' . Core::toCamelCase($func)) !== false) {
980
                $this->templatePlugins[$func]['called'] = true;
981
                $this->resolveSubTemplateDependencies($func);
982
            }
983
        }
984
        $this->templatePlugins[$function]['checked'] = true;
985
    }
986
987
    /**
988
     * Adds compiled content to the current block.
989
     *
990
     * @param string $content   the content to push
991
     * @param int    $lineCount newlines count in content, optional
992
     *
993
     * @throws CompilationException
994
     */
995
    public function push($content, $lineCount = null)
996
    {
997
        if ($lineCount === null) {
998
            $lineCount = substr_count($content, "\n");
999
        }
1000
1001
        if ($this->curBlock['buffer'] === null && count($this->stack) > 1) {
1002
            // buffer is not initialized yet (the block has just been created)
1003
            $this->stack[count($this->stack) - 2]['buffer'] .= (string)$content;
1004
            $this->curBlock['buffer'] = '';
1005
        } else {
1006
            if (!isset($this->curBlock['buffer'])) {
1007
                throw new CompilationException($this, 'The template has been closed too early, you probably have an extra block-closing tag somewhere');
1008
            }
1009
            // append current content to current block's buffer
1010
            $this->curBlock['buffer'] .= (string)$content;
1011
        }
1012
        $this->line += $lineCount;
1013
    }
1014
1015
    /**
1016
     * Sets the scope.
1017
     * set to null if the scope becomes "unstable" (i.e. too variable or unknown) so that
1018
     * variables are compiled in a more evaluative way than just $this->scope['key']
1019
     *
1020
     * @param mixed $scope    a string i.e. "level1.level2" or an array i.e. array("level1", "level2")
1021
     * @param bool  $absolute if true, the scope is set from the top level scope and not from the current scope
1022
     *
1023
     * @return array the current scope tree
1024
     */
1025
    public function setScope($scope, $absolute = false)
1026
    {
1027
        $old = $this->scopeTree;
1028
1029
        if ($scope === null) {
1030
            unset($this->scope);
1031
            $this->scope = null;
1032
        }
1033
1034
        if (is_array($scope) === false) {
1035
            $scope = explode('.', $scope);
1036
        }
1037
1038
        if ($absolute === true) {
1039
            $this->scope     = &$this->data;
1040
            $this->scopeTree = array();
1041
        }
1042
1043
        while (($bit = array_shift($scope)) !== null) {
1044
            if ($bit === '_parent' || $bit === '_') {
1045
                array_pop($this->scopeTree);
1046
                reset($this->scopeTree);
1047
                $this->scope = &$this->data;
1048
                $cnt         = count($this->scopeTree);
1049 View Code Duplication
                for ($i = 0; $i < $cnt; ++ $i) {
1050
                    $this->scope = &$this->scope[$this->scopeTree[$i]];
1051
                }
1052 View Code Duplication
            } elseif ($bit === '_root' || $bit === '__') {
1053
                $this->scope     = &$this->data;
1054
                $this->scopeTree = array();
1055
            } elseif (isset($this->scope[$bit])) {
1056
                $this->scope       = &$this->scope[$bit];
1057
                $this->scopeTree[] = $bit;
1058
            } else {
1059
                $this->scope[$bit] = array();
1060
                $this->scope       = &$this->scope[$bit];
1061
                $this->scopeTree[] = $bit;
1062
            }
1063
        }
1064
1065
        return $old;
1066
    }
1067
1068
    /**
1069
     * Adds a block to the top of the block stack.
1070
     *
1071
     * @param string $type      block type (name)
1072
     * @param array  $params    the parameters array
1073
     * @param int    $paramtype the parameters type (see mapParams), 0, 1 or 2
1074
     *
1075
     * @return string the preProcessing() method's output
1076
     */
1077
    public function addBlock($type, array $params, $paramtype)
1078
    {
1079
        if ($this->debug) {
1080
            echo 'Compiler::' . __FUNCTION__ . "\n";
1081
        }
1082
1083
        $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($type);
1084
        if (class_exists($class) === false) {
1085
            $this->getCore()->getLoader()->loadPlugin($type);
1086
        }
1087
        $params = $this->mapParams($params, array($class, 'init'), $paramtype);
1088
1089
        $this->stack[]  = array(
1090
            'type'   => $type,
1091
            'params' => $params,
1092
            'custom' => false,
1093
            'class'  => $class,
1094
            'buffer' => null
1095
        );
1096
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1097
1098
        return call_user_func(array($class, 'preProcessing'), $this, $params, '', '', $type);
1099
    }
1100
1101
    /**
1102
     * Adds a custom block to the top of the block stack.
1103
     *
1104
     * @param string $type      block type (name)
1105
     * @param array  $params    the parameters array
1106
     * @param int    $paramtype the parameters type (see mapParams), 0, 1 or 2
1107
     *
1108
     * @return string the preProcessing() method's output
1109
     */
1110
    public function addCustomBlock($type, array $params, $paramtype)
1111
    {
1112
        $callback = $this->customPlugins[$type]['callback'];
1113
        if (is_array($callback)) {
1114
            $class = is_object($callback[0]) ? get_class($callback[0]) : $callback[0];
1115
        } else {
1116
            $class = $callback;
1117
        }
1118
1119
        $params = $this->mapParams($params, array($class, 'init'), $paramtype);
1120
1121
        $this->stack[]  = array(
1122
            'type'   => $type,
1123
            'params' => $params,
1124
            'custom' => true,
1125
            'class'  => $class,
1126
            'buffer' => null
1127
        );
1128
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1129
1130
        return call_user_func(array($class, 'preProcessing'), $this, $params, '', '', $type);
1131
    }
1132
1133
    /**
1134
     * Injects a block at the top of the plugin stack without calling its preProcessing method.
1135
     * used by {else} blocks to re-add themselves after having closed everything up to their parent
1136
     *
1137
     * @param string $type   block type (name)
1138
     * @param array  $params parameters array
1139
     */
1140
    public function injectBlock($type, array $params)
1141
    {
1142
        if ($this->debug) {
1143
            echo 'Compiler::' . __FUNCTION__ . "\n";
1144
        }
1145
1146
        $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($type);
1147
        if (class_exists($class) === false) {
1148
            $this->getCore()->getLoader()->loadPlugin($type);
1149
        }
1150
        $this->stack[]  = array(
1151
            'type'   => $type,
1152
            'params' => $params,
1153
            'custom' => false,
1154
            'class'  => $class,
1155
            'buffer' => null
1156
        );
1157
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1158
    }
1159
1160
    /**
1161
     * Removes the closest-to-top block of the given type and all other
1162
     * blocks encountered while going down the block stack.
1163
     *
1164
     * @param string $type block type (name)
1165
     *
1166
     * @return string the output of all postProcessing() method's return values of the closed blocks
1167
     * @throws CompilationException
1168
     */
1169
    public function removeBlock($type)
1170
    {
1171
        if ($this->debug) {
1172
            echo 'Compiler::' . __FUNCTION__ . "\n";
1173
        }
1174
1175
        $output = '';
1176
1177
        $pluginType = $this->getPluginType($type);
1178
        if ($pluginType & Core::SMARTY_BLOCK) {
1179
            $type = 'Smartyinterface';
1180
        }
1181
        while (true) {
1182
            while ($top = array_pop($this->stack)) {
1183 View Code Duplication
                if ($top['custom']) {
1184
                    $class = $top['class'];
1185
                } else {
1186
                    $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($top['type']);
1187
                }
1188
                if (count($this->stack)) {
1189
                    $this->curBlock = &$this->stack[count($this->stack) - 1];
1190
                    $this->push(call_user_func(array(
1191
                        $class,
1192
                        'postProcessing'
1193
                    ), $this, $top['params'], '', '', $top['buffer']), 0);
1194
                } else {
1195
                    $null           = null;
1196
                    $this->curBlock = &$null;
1197
                    $output         = call_user_func(
1198
                        array(
1199
                        $class,
1200
                        'postProcessing'
1201
                        ), $this, $top['params'], '', '', $top['buffer']
1202
                    );
1203
                }
1204
1205
                if ($top['type'] === $type) {
1206
                    break 2;
1207
                }
1208
            }
1209
1210
            throw new CompilationException($this, 'Syntax malformation, a block of type "' . $type . '" was closed but was not opened');
1211
        }
1212
1213
        return $output;
1214
    }
1215
1216
    /**
1217
     * Returns a reference to the first block of the given type encountered and
1218
     * optionally closes all blocks until it finds it
1219
     * this is mainly used by {else} plugins to close everything that was opened
1220
     * between their parent and themselves.
1221
     *
1222
     * @param string $type       the block type (name)
1223
     * @param bool   $closeAlong whether to close all blocks encountered while going down the block stack or not
1224
     *
1225
     * @return mixed &array the array is as such: array('type'=>pluginName, 'params'=>parameter array,
1226
     *               'custom'=>bool defining whether it's a custom plugin or not, for internal use)
1227
     * @throws CompilationException
1228
     */
1229
    public function &findBlock($type, $closeAlong = false)
1230
    {
1231
        if ($closeAlong === true) {
1232 View Code Duplication
            while ($b = end($this->stack)) {
1233
                if ($b['type'] === $type) {
1234
                    return $this->stack[key($this->stack)];
1235
                }
1236
                $this->push($this->removeTopBlock(), 0);
1237
            }
1238
        } else {
1239
            end($this->stack);
1240 View Code Duplication
            while ($b = current($this->stack)) {
1241
                if ($b['type'] === $type) {
1242
                    return $this->stack[key($this->stack)];
1243
                }
1244
                prev($this->stack);
1245
            }
1246
        }
1247
1248
        throw new CompilationException($this, 'A parent block of type "' . $type . '" is required and can not be found');
1249
    }
1250
1251
    /**
1252
     * Returns a reference to the current block array.
1253
     *
1254
     * @return array the array is as such: array('type'=>pluginName, 'params'=>parameter array,
1255
     *                'custom'=>bool defining whether it's a custom plugin or not, for internal use)
1256
     */
1257
    public function &getCurrentBlock()
1258
    {
1259
        return $this->curBlock;
1260
    }
1261
1262
    /**
1263
     * Removes the block at the top of the stack and calls its postProcessing() method.
1264
     *
1265
     * @return string the postProcessing() method's output
1266
     * @throws CompilationException
1267
     */
1268
    public function removeTopBlock()
1269
    {
1270
        if ($this->debug) {
1271
            echo 'Compiler::' . __FUNCTION__ . "\n";
1272
        }
1273
1274
        $o = array_pop($this->stack);
1275
        if ($o === null) {
1276
            throw new CompilationException($this, 'Syntax malformation, a block of unknown type was closed but was not opened.');
1277
        }
1278 View Code Duplication
        if ($o['custom']) {
1279
            $class = $o['class'];
1280
        } else {
1281
            $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($o['type']);
1282
        }
1283
1284
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1285
1286
        return call_user_func(array($class, 'postProcessing'), $this, $o['params'], '', '', $o['buffer']);
1287
    }
1288
1289
    /**
1290
     * Returns the compiled parameters (for example a variable's compiled parameter will be "$this->scope['key']") out
1291
     * of the given parameter array.
1292
     *
1293
     * @param array $params parameter array
1294
     *
1295
     * @return array filtered parameters
1296
     */
1297 View Code Duplication
    public function getCompiledParams(array $params)
1298
    {
1299
        foreach ($params as $k => $p) {
1300
            if (is_array($p)) {
1301
                $params[$k] = $p[0];
1302
            }
1303
        }
1304
1305
        return $params;
1306
    }
1307
1308
    /**
1309
     * Returns the real parameters (for example a variable's real parameter will be its key, etc) out of the given
1310
     * parameter array.
1311
     *
1312
     * @param array $params parameter array
1313
     *
1314
     * @return array filtered parameters
1315
     */
1316 View Code Duplication
    public function getRealParams(array $params)
1317
    {
1318
        foreach ($params as $k => $p) {
1319
            if (is_array($p)) {
1320
                $params[$k] = $p[1];
1321
            }
1322
        }
1323
1324
        return $params;
1325
    }
1326
1327
    /**
1328
     * Returns the token of each parameter out of the given parameter array.
1329
     *
1330
     * @param array $params parameter array
1331
     *
1332
     * @return array tokens
1333
     */
1334 View Code Duplication
    public function getParamTokens(array $params)
1335
    {
1336
        foreach ($params as $k => $p) {
1337
            if (is_array($p)) {
1338
                $params[$k] = isset($p[2]) ? $p[2] : 0;
1339
            }
1340
        }
1341
1342
        return $params;
1343
    }
1344
1345
    /**
1346
     * Entry point of the parser, it redirects calls to other parse* functions.
1347
     *
1348
     * @param string $in            the string within which we must parse something
1349
     * @param int    $from          the starting offset of the parsed area
1350
     * @param int    $to            the ending offset of the parsed area
1351
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
1352
     *                              default
1353
     * @param string $curBlock      the current parser-block being processed
1354
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
1355
     *                              or null by default
1356
     *
1357
     * @return string parsed values
1358
     * @throws CompilationException
1359
     */
1360
    protected function parse($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
1361
    {
1362
        if ($this->debug) {
1363
            echo 'Compiler::' . __FUNCTION__ . "\n";
1364
        }
1365
1366
        if ($to === null) {
1367
            $to = strlen($in);
1368
        }
1369
        $first = substr($in, $from, 1);
1370
1371
        if ($first === false) {
1372
            throw new CompilationException($this, 'Unexpected EOF, a template tag was not closed');
1373
        }
1374
1375
        while ($first === ' ' || $first === "\n" || $first === "\t" || $first === "\r") {
1376 View Code Duplication
            if ($curBlock === 'root' && substr($in, $from, strlen($this->rd)) === $this->rd) {
1377
                // end template tag
1378
                $pointer += strlen($this->rd);
1379
                if ($this->debug) {
1380
                    echo 'TEMPLATE PARSING ENDED' . "\n";
1381
                }
1382
1383
                return false;
1384
            }
1385
            ++ $from;
1386
            if ($pointer !== null) {
1387
                ++ $pointer;
1388
            }
1389
            if ($from >= $to) {
1390
                if (is_array($parsingParams)) {
1391
                    return $parsingParams;
1392
                } else {
1393
                    return '';
1394
                }
1395
            }
1396
            $first = $in[$from];
1397
        }
1398
1399
        $substr = substr($in, $from, $to - $from);
1400
1401
        if ($this->debug) {
1402
            echo 'PARSE CALL : PARSING "' . htmlentities(substr($in, $from, min($to - $from, 50))) . (($to - $from) > 50 ? '...' : '') . '" @ ' . $from . ':' . $to . ' in ' . $curBlock . ' : pointer=' . $pointer . "\n";
1403
        }
1404
        $parsed = '';
1405
1406
        if ($curBlock === 'root' && $first === '*') {
1407
            $src      = $this->getTemplateSource();
1408
            $startpos = $this->getPointer() - strlen($this->ld);
1409
            if (substr($src, $startpos, strlen($this->ld)) === $this->ld) {
1410
                if ($startpos > 0) {
1411
                    do {
1412
                        $char = substr($src, -- $startpos, 1);
1413
                        if ($char == "\n") {
1414
                            ++ $startpos;
1415
                            $whitespaceStart = true;
1416
                            break;
1417
                        }
1418
                    }
1419
                    while ($startpos > 0 && ($char == ' ' || $char == "\t"));
1420
                }
1421
1422
                if (!isset($whitespaceStart)) {
1423
                    $startpos = $this->getPointer();
1424
                } else {
1425
                    $pointer -= $this->getPointer() - $startpos;
1426
                }
1427
1428
                if ($this->allowNestedComments && strpos($src, $this->ld . '*', $this->getPointer()) !== false) {
1429
                    $comOpen  = $this->ld . '*';
1430
                    $comClose = '*' . $this->rd;
1431
                    $level    = 1;
1432
                    $ptr      = $this->getPointer();
1433
1434
                    while ($level > 0 && $ptr < strlen($src)) {
1435
                        $open  = strpos($src, $comOpen, $ptr);
1436
                        $close = strpos($src, $comClose, $ptr);
1437
1438
                        if ($open !== false && $close !== false) {
1439
                            if ($open < $close) {
1440
                                $ptr = $open + strlen($comOpen);
1441
                                ++ $level;
1442
                            } else {
1443
                                $ptr = $close + strlen($comClose);
1444
                                -- $level;
1445
                            }
1446
                        } elseif ($open !== false) {
1447
                            $ptr = $open + strlen($comOpen);
1448
                            ++ $level;
1449
                        } elseif ($close !== false) {
1450
                            $ptr = $close + strlen($comClose);
1451
                            -- $level;
1452
                        } else {
1453
                            $ptr = strlen($src);
1454
                        }
1455
                    }
1456
                    $endpos = $ptr - strlen('*' . $this->rd);
1457 View Code Duplication
                } else {
1458
                    $endpos = strpos($src, '*' . $this->rd, $startpos);
1459
                    if ($endpos == false) {
1460
                        throw new CompilationException($this, 'Un-ended comment');
1461
                    }
1462
                }
1463
                $pointer += $endpos - $startpos + strlen('*' . $this->rd);
1464
                if (isset($whitespaceStart) && preg_match('#^[\t ]*\r?\n#', substr($src, $endpos + strlen('*' . $this->rd)), $m)) {
1465
                    $pointer += strlen($m[0]);
1466
                    $this->curBlock['buffer'] = substr($this->curBlock['buffer'], 0, strlen($this->curBlock['buffer']) - ($this->getPointer() - $startpos - strlen($this->ld)));
1467
                }
1468
1469
                return false;
1470
            }
1471
        }
1472
1473
        if ($first === '$') {
1474
            // var
1475
            $out    = $this->parseVar($in, $from, $to, $parsingParams, $curBlock, $pointer);
1476
            $parsed = 'var';
1477
        } elseif ($first === '%' && preg_match('#^%[a-z_\\\\]#i', $substr)) {
1478
            // Short constant
1479
            $out = $this->parseConst($in, $from, $to, $parsingParams, $curBlock, $pointer);
1480
        } elseif (($first === '"' || $first === "'") && !(is_array($parsingParams) && preg_match('#^([\'"])[a-z0-9_]+\1\s*=>?(?:\s+|[^=])#i', $substr))) {
1481
            // string
1482
            $out = $this->parseString($in, $from, $to, $parsingParams, $curBlock, $pointer);
1483
        } elseif (preg_match('/^\\\\?[a-z_](?:\\\\?[a-z0-9_]+)*(?:::[a-z_][a-z0-9_]*)?(' . (is_array($parsingParams) || $curBlock != 'root' ? '' : '\s+[^(]|') . '\s*\(|\s*' . $this->rdr . '|\s*;)/i', $substr)) {
1484
            // func
1485
            $out    = $this->parseFunction($in, $from, $to, $parsingParams, $curBlock, $pointer);
1486
            $parsed = 'func';
1487
        } elseif ($first === ';') {
1488
            // instruction end
1489
            if ($this->debug) {
1490
                echo 'END OF INSTRUCTION' . "\n";
1491
            }
1492
            if ($pointer !== null) {
1493
                ++ $pointer;
1494
            }
1495
1496
            return $this->parse($in, $from + 1, $to, false, 'root', $pointer);
1497
        } elseif ($curBlock === 'root' && preg_match('#^/([a-z_][a-z0-9_]*)?#i', $substr, $match)) {
1498
            // close block
1499 View Code Duplication
            if (!empty($match[1]) && $match[1] == 'else') {
1500
                throw new CompilationException($this, 'Else blocks must not be closed explicitly, they are automatically closed when their parent block is closed');
1501
            }
1502 View Code Duplication
            if (!empty($match[1]) && $match[1] == 'elseif') {
1503
                throw new CompilationException($this, 'Elseif blocks must not be closed explicitly, they are automatically closed when their parent block is closed or a new else/elseif block is declared after them');
1504
            }
1505
            if ($pointer !== null) {
1506
                $pointer += strlen($match[0]);
1507
            }
1508
            if (empty($match[1])) {
1509
                if ($this->curBlock['type'] == 'else' || $this->curBlock['type'] == 'elseif') {
1510
                    $pointer -= strlen($match[0]);
1511
                }
1512
                if ($this->debug) {
1513
                    echo 'TOP BLOCK CLOSED' . "\n";
1514
                }
1515
1516
                return $this->removeTopBlock();
1517
            } else {
1518
                if ($this->debug) {
1519
                    echo 'BLOCK OF TYPE ' . $match[1] . ' CLOSED' . "\n";
1520
                }
1521
1522
                return $this->removeBlock($match[1]);
1523
            }
1524 View Code Duplication
        } elseif ($curBlock === 'root' && substr($substr, 0, strlen($this->rd)) === $this->rd) {
1525
            // end template tag
1526
            if ($this->debug) {
1527
                echo 'TAG PARSING ENDED' . "\n";
1528
            }
1529
            $pointer += strlen($this->rd);
1530
1531
            return false;
1532
        } elseif (is_array($parsingParams) && preg_match('#^(([\'"]?)[a-z0-9_]+\2\s*=' . ($curBlock === 'array' ? '>?' : '') . ')(?:\s+|[^=]).*#i', $substr, $match)) {
1533
            // named parameter
1534
            if ($this->debug) {
1535
                echo 'NAMED PARAM FOUND' . "\n";
1536
            }
1537
            $len = strlen($match[1]);
1538
            while (substr($in, $from + $len, 1) === ' ') {
1539
                ++ $len;
1540
            }
1541
            if ($pointer !== null) {
1542
                $pointer += $len;
1543
            }
1544
1545
            $output = array(
1546
                trim($match[1], " \t\r\n=>'\""),
1547
                $this->parse($in, $from + $len, $to, false, 'namedparam', $pointer)
1548
            );
1549
1550
            $parsingParams[] = $output;
1551
1552
            return $parsingParams;
1553
        } elseif (preg_match('#^(\\\\?[a-z_](?:\\\\?[a-z0-9_]+)*::\$[a-z0-9_]+)#i', $substr, $match)) {
1554
            // static member access
1555
            $parsed = 'var';
1556
            if (is_array($parsingParams)) {
1557
                $parsingParams[] = array($match[1], $match[1]);
1558
                $out             = $parsingParams;
1559
            } else {
1560
                $out = $match[1];
1561
            }
1562
            $pointer += strlen($match[1]);
1563
        } elseif ($substr !== '' && (is_array($parsingParams) || $curBlock === 'namedparam' || $curBlock === 'condition' || $curBlock === 'expression')) {
1564
            // unquoted string, bool or number
1565
            $out = $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
1566 View Code Duplication
        } else {
1567
            // parse error
1568
            throw new CompilationException($this, 'Parse error in "' . substr($in, $from, $to - $from) . '"');
1569
        }
1570
1571
        if (empty($out)) {
1572
            return '';
1573
        }
1574
1575
        $substr = substr($in, $pointer, $to - $pointer);
1576
1577
        // var parsed, check if any var-extension applies
1578
        if ($parsed === 'var') {
1579
            if (preg_match('#^\s*([/%+*-])\s*([a-z0-9]|\$)#i', $substr, $match)) {
1580
                if ($this->debug) {
1581
                    echo 'PARSING POST-VAR EXPRESSION ' . $substr . "\n";
1582
                }
1583
                // parse expressions
1584
                $pointer += strlen($match[0]) - 1;
1585
                if (is_array($parsingParams)) {
1586
                    if ($match[2] == '$') {
1587
                        $expr = $this->parseVar($in, $pointer, $to, array(), $curBlock, $pointer);
1588
                    } else {
1589
                        $expr = $this->parse($in, $pointer, $to, array(), 'expression', $pointer);
1590
                    }
1591
                    $out[count($out) - 1][0] .= $match[1] . $expr[0][0];
1592
                    $out[count($out) - 1][1] .= $match[1] . $expr[0][1];
1593
                } else {
1594
                    if ($match[2] == '$') {
1595
                        $expr = $this->parseVar($in, $pointer, $to, false, $curBlock, $pointer);
1596
                    } else {
1597
                        $expr = $this->parse($in, $pointer, $to, false, 'expression', $pointer);
1598
                    }
1599
                    if (is_array($out) && is_array($expr)) {
1600
                        $out[0] .= $match[1] . $expr[0];
1601
                        $out[1] .= $match[1] . $expr[1];
1602
                    } elseif (is_array($out)) {
1603
                        $out[0] .= $match[1] . $expr;
1604
                        $out[1] .= $match[1] . $expr;
1605
                    } elseif (is_array($expr)) {
1606
                        $out .= $match[1] . $expr[0];
1607
                    } else {
1608
                        $out .= $match[1] . $expr;
1609
                    }
1610
                }
1611
            } elseif ($curBlock === 'root' && preg_match('#^(\s*(?:[+/*%-.]=|=|\+\+|--)\s*)(.*)#s', $substr, $match)) {
1612
                if ($this->debug) {
1613
                    echo 'PARSING POST-VAR ASSIGNMENT ' . $substr . "\n";
1614
                }
1615
                // parse assignment
1616
                $value    = $match[2];
1617
                $operator = trim($match[1]);
1618
                if (substr($value, 0, 1) == '=') {
1619
                    throw new CompilationException($this, 'Unexpected "=" in <em>' . $substr . '</em>');
1620
                }
1621
1622
                if ($pointer !== null) {
1623
                    $pointer += strlen($match[1]);
1624
                }
1625
1626
                if ($operator !== '++' && $operator !== '--') {
1627
                    $parts = array();
1628
                    $ptr   = 0;
1629
                    $parts = $this->parse($value, 0, strlen($value), $parts, 'condition', $ptr);
1630
                    $pointer += $ptr;
1631
1632
                    // load if plugin
1633
                    try {
1634
                        $this->getPluginType('if');
1635
                    }
1636
                    catch (Exception $e) {
1637
                        throw new CompilationException($this, 'Assignments require the "if" plugin to be accessible');
1638
                    }
1639
1640
                    $parts  = $this->mapParams($parts, array(Core::NAMESPACE_PLUGINS_BLOCKS . 'PluginIf', 'init'), 1);
1641
                    $tokens = $this->getParamTokens($parts);
1642
                    $parts  = $this->getCompiledParams($parts);
1643
1644
                    $value = PluginIf::replaceKeywords($parts['*'], $tokens['*'], $this);
1645
                    $echo  = '';
1646
                } else {
1647
                    $value = array();
1648
                    $echo  = 'echo ';
1649
                }
1650
1651
                if ($this->autoEscape) {
1652
                    $out = preg_replace('#\(is_string\(\$tmp=(.+?)\) \? htmlspecialchars\(\$tmp, ENT_QUOTES, \$this->charset\) : \$tmp\)#', '$1', $out);
1653
                }
1654
                $out = self::PHP_OPEN . $echo . $out . $operator . implode(' ', $value) . self::PHP_CLOSE;
1655
            } elseif ($curBlock === 'array' && is_array($parsingParams) && preg_match('#^(\s*=>?\s*)#', $substr, $match)) {
1656
                // parse namedparam with var as name (only for array)
1657
                if ($this->debug) {
1658
                    echo 'VARIABLE NAMED PARAM (FOR ARRAY) FOUND' . "\n";
1659
                }
1660
                $len = strlen($match[1]);
1661
                $var = $out[count($out) - 1];
1662
                $pointer += $len;
1663
1664
                $output = array($var[0], $this->parse($substr, $len, null, false, 'namedparam', $pointer));
1665
1666
                $parsingParams[] = $output;
1667
1668
                return $parsingParams;
1669
            }
1670
        }
1671
1672
        if ($curBlock !== 'modifier' && ($parsed === 'func' || $parsed === 'var') && preg_match('#^(\|@?[a-z0-9_]+(:.*)?)+#i', $substr, $match)) {
1673
            // parse modifier on funcs or vars
1674
            $srcPointer = $pointer;
1675
            if (is_array($parsingParams)) {
1676
                $tmp                     = $this->replaceModifiers(
1677
                    array(
1678
                    null,
1679
                    null,
1680
                    $out[count($out) - 1][0],
1681
                    $match[0]
1682
                    ), $curBlock, $pointer
1683
                );
1684
                $out[count($out) - 1][0] = $tmp;
1685
                $out[count($out) - 1][1] .= substr($substr, $srcPointer, $srcPointer - $pointer);
1686
            } else {
1687
                $out = $this->replaceModifiers(array(null, null, $out, $match[0]), $curBlock, $pointer);
1688
            }
1689
        }
1690
1691
        // func parsed, check if any func-extension applies
1692
        if ($parsed === 'func' && preg_match('#^->[a-z0-9_]+(\s*\(.+|->[a-z_].*)?#is', $substr, $match)) {
1693
            // parse method call or property read
1694
            $ptr = 0;
1695
1696
            if (is_array($parsingParams)) {
1697
                $output = $this->parseMethodCall($out[count($out) - 1][1], $match[0], $curBlock, $ptr);
1698
1699
                $out[count($out) - 1][0] = $output;
1700
                $out[count($out) - 1][1] .= substr($match[0], 0, $ptr);
1701
            } else {
1702
                $out = $this->parseMethodCall($out, $match[0], $curBlock, $ptr);
1703
            }
1704
1705
            $pointer += $ptr;
1706
        }
1707
1708
        if ($curBlock === 'root' && substr($out, 0, strlen(self::PHP_OPEN)) !== self::PHP_OPEN) {
1709
            return self::PHP_OPEN . 'echo ' . $out . ';' . self::PHP_CLOSE;
1710
        }
1711
1712
        return $out;
1713
    }
1714
1715
    /**
1716
     * Parses a function call.
1717
     *
1718
     * @param string $in            the string within which we must parse something
1719
     * @param int    $from          the starting offset of the parsed area
1720
     * @param int    $to            the ending offset of the parsed area
1721
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
1722
     *                              default
1723
     * @param string $curBlock      the current parser-block being processed
1724
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
1725
     *                              or null by default
1726
     *
1727
     * @return string parsed values
1728
     * @throws CompilationException
1729
     * @throws Exception
1730
     * @throws SecurityException
1731
     */
1732
    protected function parseFunction($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
1733
    {
1734
        $output = '';
1735
        $cmdstr = substr($in, $from, $to - $from);
1736
        preg_match('/^(\\\\?[a-z_](?:\\\\?[a-z0-9_]+)*(?:::[a-z_][a-z0-9_]*)?)(\s*' . $this->rdr . '|\s*;)?/i', $cmdstr, $match);
1737
1738 View Code Duplication
        if (empty($match[1])) {
1739
            throw new CompilationException($this, 'Parse error, invalid function name : ' . substr($cmdstr, 0, 15));
1740
        }
1741
1742
        $func = $match[1];
1743
1744
        if (!empty($match[2])) {
1745
            $cmdstr = $match[1];
1746
        }
1747
1748
        if ($this->debug) {
1749
            echo 'FUNC FOUND (' . $func . ')' . "\n";
1750
        }
1751
1752
        $paramsep = '';
1753
1754
        if (is_array($parsingParams) || $curBlock != 'root') {
1755
            $paramspos = strpos($cmdstr, '(');
1756
            $paramsep  = ')';
1757
        } elseif (preg_match_all('#^\s*[\\\\:a-z0-9_]+(\s*\(|\s+[^(])#i', $cmdstr, $match, PREG_OFFSET_CAPTURE)) {
1758
            $paramspos = $match[1][0][1];
1759
            $paramsep  = substr($match[1][0][0], - 1) === '(' ? ')' : '';
1760
            if ($paramsep === ')') {
1761
                $paramspos += strlen($match[1][0][0]) - 1;
1762
                if (substr($cmdstr, 0, 2) === 'if' || substr($cmdstr, 0, 6) === 'elseif') {
1763
                    $paramsep = '';
1764
                    if (strlen($match[1][0][0]) > 1) {
1765
                        -- $paramspos;
1766
                    }
1767
                }
1768
            }
1769
        } else {
1770
            $paramspos = false;
1771
        }
1772
1773
        $state = 0;
1774
1775
        if ($paramspos === false) {
1776
            $params = array();
1777
1778
            if ($curBlock !== 'root') {
1779
                return $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
1780
            }
1781
        } else {
1782
            if ($curBlock === 'condition') {
1783
                // load if plugin
1784
                $this->getPluginType('if');
1785
1786
                if (PluginIf::replaceKeywords(array($func), array(self::T_UNQUOTED_STRING), $this) !== array($func)) {
1787
                    return $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
1788
                }
1789
            }
1790
            $whitespace = strlen(substr($cmdstr, strlen($func), $paramspos - strlen($func)));
1791
            $paramstr   = substr($cmdstr, $paramspos + 1);
1792 View Code Duplication
            if (substr($paramstr, - 1, 1) === $paramsep) {
1793
                $paramstr = substr($paramstr, 0, - 1);
1794
            }
1795
1796
            if (strlen($paramstr) === 0) {
1797
                $params   = array();
1798
                $paramstr = '';
1799
            } else {
1800
                $ptr    = 0;
1801
                $params = array();
1802
                if ($func === 'empty') {
1803
                    $params = $this->parseVar($paramstr, $ptr, strlen($paramstr), $params, 'root', $ptr);
1804
                } else {
1805
                    while ($ptr < strlen($paramstr)) {
1806
                        while (true) {
1807
                            if ($ptr >= strlen($paramstr)) {
1808
                                break 2;
1809
                            }
1810
1811
                            if ($func !== 'if' && $func !== 'elseif' && $paramstr[$ptr] === ')') {
1812
                                if ($this->debug) {
1813
                                    echo 'PARAM PARSING ENDED, ")" FOUND, POINTER AT ' . $ptr . "\n";
1814
                                }
1815
                                break 2;
1816
                            } elseif ($paramstr[$ptr] === ';') {
1817
                                ++ $ptr;
1818
                                if ($this->debug) {
1819
                                    echo 'PARAM PARSING ENDED, ";" FOUND, POINTER AT ' . $ptr . "\n";
1820
                                }
1821
                                break 2;
1822
                            } elseif ($func !== 'if' && $func !== 'elseif' && $paramstr[$ptr] === '/') {
1823
                                if ($this->debug) {
1824
                                    echo 'PARAM PARSING ENDED, "/" FOUND, POINTER AT ' . $ptr . "\n";
1825
                                }
1826
                                break 2;
1827
                            } elseif (substr($paramstr, $ptr, strlen($this->rd)) === $this->rd) {
1828
                                if ($this->debug) {
1829
                                    echo 'PARAM PARSING ENDED, RIGHT DELIMITER FOUND, POINTER AT ' . $ptr . "\n";
1830
                                }
1831
                                break 2;
1832
                            }
1833
1834
                            if ($paramstr[$ptr] === ' ' || $paramstr[$ptr] === ',' || $paramstr[$ptr] === "\r" || $paramstr[$ptr] === "\n" || $paramstr[$ptr] === "\t") {
1835
                                ++ $ptr;
1836
                            } else {
1837
                                break;
1838
                            }
1839
                        }
1840
1841
                        if ($this->debug) {
1842
                            echo 'FUNC START PARAM PARSING WITH POINTER AT ' . $ptr . "\n";
1843
                        }
1844
1845
                        if ($func === 'if' || $func === 'elseif' || $func === 'tif') {
1846
                            $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'condition', $ptr);
1847
                        } elseif ($func === 'array') {
1848
                            $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'array', $ptr);
1849
                        } else {
1850
                            $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'function', $ptr);
1851
                        }
1852
1853 View Code Duplication
                        if ($this->debug) {
1854
                            echo 'PARAM PARSED, POINTER AT ' . $ptr . ' (' . substr($paramstr, $ptr - 1, 3) . ')' . "\n";
1855
                        }
1856
                    }
1857
                }
1858
                $paramstr = substr($paramstr, 0, $ptr);
1859
                $state    = 0;
1860
                foreach ($params as $k => $p) {
1861
                    if (is_array($p) && is_array($p[1])) {
1862
                        $state |= 2;
1863
                    } else {
1864
                        if (($state & 2) && preg_match('#^(["\'])(.+?)\1$#', $p[0], $m) && $func !== 'array') {
1865
                            $params[$k] = array($m[2], array('true', 'true'));
1866
                        } else {
1867
                            if ($state & 2 && $func !== 'array') {
1868
                                throw new CompilationException($this, 'You can not use an unnamed parameter after a named one');
1869
                            }
1870
                            $state |= 1;
1871
                        }
1872
                    }
1873
                }
1874
            }
1875
        }
1876
1877
        if ($pointer !== null) {
1878
            $pointer += (isset($paramstr) ? strlen($paramstr) : 0) + (')' === $paramsep ? 2 : ($paramspos === false ? 0 : 1)) + strlen($func) + (isset($whitespace) ? $whitespace : 0);
1879
            if ($this->debug) {
1880
                echo 'FUNC ADDS ' . ((isset($paramstr) ? strlen($paramstr) : 0) + (')' === $paramsep ? 2 : ($paramspos === false ? 0 : 1)) + strlen($func)) . ' TO POINTER' . "\n";
1881
            }
1882
        }
1883
1884
        if ($curBlock === 'method' || $func === 'do' || strstr($func, '::') !== false) {
1885
            // handle static method calls with security policy
1886
            if (strstr($func, '::') !== false && $this->securityPolicy !== null && $this->securityPolicy->isMethodAllowed(explode('::', strtolower($func))) !== true) {
1887
                throw new SecurityException('Call to a disallowed php function : ' . $func);
1888
            }
1889
            $pluginType = Core::NATIVE_PLUGIN;
1890
        } else {
1891
            $pluginType = $this->getPluginType($func);
1892
        }
1893
1894
        // Blocks plugin
1895
        if ($pluginType & Core::BLOCK_PLUGIN) {
1896
            if ($curBlock !== 'root' || is_array($parsingParams)) {
1897
                throw new CompilationException($this, 'Block plugins can not be used as other plugin\'s arguments');
1898
            }
1899
            if ($pluginType & Core::CUSTOM_PLUGIN) {
1900
                return $this->addCustomBlock($func, $params, $state);
1901
            } else {
1902
                return $this->addBlock($func, $params, $state);
1903
            }
1904
        } elseif ($pluginType & Core::SMARTY_BLOCK) {
1905
            if ($curBlock !== 'root' || is_array($parsingParams)) {
1906
                throw new CompilationException($this, 'Block plugins can not be used as other plugin\'s arguments');
1907
            }
1908
1909
            if ($state & 2) {
1910
                array_unshift($params, array('__functype', array($pluginType, $pluginType)));
1911
                array_unshift($params, array('__funcname', array($func, $func)));
1912
            } else {
1913
                array_unshift($params, array($pluginType, $pluginType));
1914
                array_unshift($params, array($func, $func));
1915
            }
1916
1917
            return $this->addBlock('smartyinterface', $params, $state);
1918
        }
1919
1920
        // Native & Smarty plugins
1921
        if ($pluginType & Core::NATIVE_PLUGIN || $pluginType & Core::SMARTY_FUNCTION || $pluginType & Core::SMARTY_BLOCK) {
1922
            $params = $this->mapParams($params, null, $state);
1923
        } // PHP class plugin
1924
        elseif ($pluginType & Core::CLASS_PLUGIN) {
1925
            if ($pluginType & Core::CUSTOM_PLUGIN) {
1926
                $params = $this->mapParams(
1927
                    $params, array(
1928
                    $this->customPlugins[$func]['class'],
1929
                    $this->customPlugins[$func]['function']
1930
                ), $state);
1931
            } else {
1932
                if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
1933
                    $params = $this->mapParams($params, array(
1934
                        'Plugin' . Core::toCamelCase($func),
1935
                        ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process'
1936
                    ), $state);
1937
                } elseif (class_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func)) !== false) {
1938
                    $params = $this->mapParams($params, array(
1939
                        Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func),
1940
                        ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process'
1941
                    ), $state);
1942 View Code Duplication
                } else {
1943
                    $params = $this->mapParams($params, array(
1944
                        Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func),
1945
                        ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process'
1946
                    ), $state);
1947
                }
1948
            }
1949
        } // PHP function plugin
1950
        elseif ($pluginType & Core::FUNC_PLUGIN) {
1951
            if ($pluginType & Core::CUSTOM_PLUGIN) {
1952
                $params = $this->mapParams($params, $this->customPlugins[$func]['callback'], $state);
1953
            } else {
1954
                // Custom plugin
1955
                if (function_exists('Plugin' . Core::toCamelCase($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ?
1956
                        'Compile' : '')) !== false) {
1957
                    $params = $this->mapParams($params, 'Plugin' . Core::toCamelCase($func) . (($pluginType &
1958
                            Core::COMPILABLE_PLUGIN) ? 'Compile' : ''), $state);
1959
                } // Builtin helper plugin
1960
                elseif (function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) . (
1961
                    ($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : '')) !== false) {
1962
                    $params = $this->mapParams($params, Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase
1963
                        ($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : ''), $state);
1964
                } // Builtin function plugin
1965 View Code Duplication
                else {
1966
                    $params = $this->mapParams($params, Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase
1967
                        ($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : ''), $state);
1968
                }
1969
            }
1970
        } // Smarty modifier
1971
        elseif ($pluginType & Core::SMARTY_MODIFIER) {
1972
            $output = 'smarty_modifier_' . $func . '(' . implode(', ', $params) . ')';
1973
        } // Proxy plugin
1974
        elseif ($pluginType & Core::PROXY_PLUGIN) {
1975
            $params = $this->mapParams($params, $this->getCore()->getPluginProxy()->getCallback($func), $state);
1976
        } // Template plugin
1977
        elseif ($pluginType & Core::TEMPLATE_PLUGIN) {
1978
            // transforms the parameter array from (x=>array('paramname'=>array(values))) to (paramname=>array(values))
1979
            $map = array();
1980
            foreach ($this->templatePlugins[$func]['params'] as $param => $defValue) {
1981
                if ($param == 'rest') {
1982
                    $param = '*';
1983
                }
1984
                $hasDefault = $defValue !== null;
1985
                if ($defValue === 'null') {
1986
                    $defValue = null;
1987
                } elseif ($defValue === 'false') {
1988
                    $defValue = false;
1989
                } elseif ($defValue === 'true') {
1990
                    $defValue = true;
1991
                } elseif (preg_match('#^([\'"]).*?\1$#', $defValue)) {
1992
                    $defValue = substr($defValue, 1, - 1);
1993
                }
1994
                $map[] = array($param, $hasDefault, $defValue);
1995
            }
1996
1997
            $params = $this->mapParams($params, null, $state, $map);
1998
        }
1999
2000
        // only keep php-syntax-safe values for non-block plugins
2001
        $tokens = array();
2002
        foreach ($params as $k => $p) {
2003
            $tokens[$k] = isset($p[2]) ? $p[2] : 0;
2004
            $params[$k] = $p[0];
2005
        }
2006
2007
        // Native plugin
2008
        if ($pluginType & Core::NATIVE_PLUGIN) {
2009
            if ($func === 'do') {
2010
                $output = '';
2011
                if (isset($params['*'])) {
2012
                    $output = implode(';', $params['*']) . ';';
2013
                }
2014
2015
                if (is_array($parsingParams) || $curBlock !== 'root') {
2016
                    throw new CompilationException($this, 'Do can not be used inside another function or block');
2017
                }
2018
2019
                return self::PHP_OPEN . $output . self::PHP_CLOSE;
2020
            } else {
2021
                if (isset($params['*'])) {
2022
                    $output = $func . '(' . implode(', ', $params['*']) . ')';
2023
                } else {
2024
                    $output = $func . '()';
2025
                }
2026
            }
2027
        } // Block class OR Function class
2028
        elseif ($pluginType & Core::CLASS_PLUGIN || ($pluginType & Core::FUNC_PLUGIN && $pluginType & Core::CLASS_PLUGIN)) {
2029
            if ($pluginType & Core::COMPILABLE_PLUGIN) {
2030
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2031
                    $callback = $this->customPlugins[$func]['callback'];
2032
                    if (!is_array($callback)) {
2033
                        if (!method_exists($callback, 'compile')) {
2034
                            throw new Exception('Custom plugin ' . $func . ' must implement the "compile" method to be compilable, or you should provide a full callback to the method to use');
2035
                        }
2036
                        if (($ref = new ReflectionMethod($callback, 'compile')) && $ref->isStatic()) {
2037
                            $funcCompiler = array($callback, 'compile');
2038
                        } else {
2039
                            $funcCompiler = array(new $callback(), 'compile');
2040
                        }
2041
                    } else {
2042
                        $funcCompiler = $callback;
2043
                    }
2044
                } else {
2045
                    if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
2046
                        $funcCompiler = array('Plugin' . Core::toCamelCase($func), 'compile');
2047
                    } elseif (class_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func)) !== false) {
2048
                        $funcCompiler = array(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func), 'compile');
2049
                    } else {
2050
                        $funcCompiler = array(
2051
                            Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func),
2052
                            'compile'
2053
                        );
2054
                    }
2055
                    array_unshift($params, $this);
2056
                }
2057
                // @TODO: Is it a real fix ?
2058
                if ($func === 'tif') {
2059
                    $params[] = $tokens;
2060
                }
2061
                $output = call_user_func_array($funcCompiler, $params);
2062
            } else {
2063
                $params = self::implode_r($params);
2064
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2065
                    $callback = $this->customPlugins[$func]['callback'];
2066
                    if (!is_array($callback)) {
2067
                        if (!method_exists($callback, 'process')) {
2068
                            throw new Exception('Custom plugin ' . $func . ' must implement the "process" method to be usable, or you should provide a full callback to the method to use');
2069
                        }
2070
                        if (is_object($callback)) {
2071
                            $callback = get_class($callback);
2072
                        }
2073
                        if (($ref = new ReflectionMethod($callback, 'process')) && $ref->isStatic()) {
2074
                            $output = 'call_user_func(array(\'' . $callback . '\', \'process\'), ' . $params . ')';
2075
                        } else {
2076
                            $output = 'call_user_func(array($this->getObjectPlugin(\'' . $callback . '\'), \'process\'), ' . $params . ')';
2077
                        }
2078 View Code Duplication
                    } elseif (is_object($callback[0])) {
2079
                        $output = 'call_user_func(array($this->plugins[\'' . $func . '\'][\'callback\'][0], \'' . $callback[1] . '\'), ' . $params . ')';
2080
                    } elseif (($ref = new ReflectionMethod($callback[0], $callback[1])) && $ref->isStatic()) {
2081
                        $output = 'call_user_func(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), ' . $params . ')';
2082 View Code Duplication
                    } else {
2083
                        $output = 'call_user_func(array($this->getObjectPlugin(\'' . $callback[0] . '\'), \'' . $callback[1] . '\'), ' . $params . ')';
2084
                    }
2085
                    if (empty($params)) {
2086
                        $output = substr($output, 0, - 3) . ')';
2087
                    }
2088
                } else {
2089
                    if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
2090
                        $output = '$this->classCall(\'Plugin' . $func . '\', array(' . $params . '))';
2091 View Code Duplication
                    } elseif (class_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func)) !== false) {
2092
                        $output = '$this->classCall(\'' . Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . $func . '\', 
2093
                        array(' . $params . '))';
2094
                    } else {
2095
                        $output = '$this->classCall(\'' . $func . '\', array(' . $params . '))';
2096
                    }
2097
                }
2098
            }
2099
        } // Function plugin only (cannot be a class)
2100
        elseif ($pluginType & Core::FUNC_PLUGIN) {
2101
            if ($pluginType & Core::COMPILABLE_PLUGIN) {
2102
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2103
                    $funcCompiler = $this->customPlugins[$func]['callback'];
2104
                } else {
2105
                    // Custom plugin
2106
                    if (function_exists('Plugin' . Core::toCamelCase($func) . 'Compile') !== false) {
2107
                        $funcCompiler = 'Plugin' . Core::toCamelCase($func) . 'Compile';
2108
                    } // Builtin helper plugin
2109 View Code Duplication
                    elseif (function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) . 'Compile') !== false) {
2110
                        $funcCompiler = Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) .
2111
                            'Compile';
2112
                    } // Builtin function plugin
2113
                    else {
2114
                        $funcCompiler = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) .
2115
                            'Compile';
2116
                    }
2117
                }
2118
                array_unshift($params, $this);
2119
                // @TODO: Is it a real fix ?
2120
                if ($func === 'tif') {
2121
                    $params[] = $tokens;
2122
                }
2123
                $output = call_user_func_array($funcCompiler, $params);
2124
            } else {
2125
                array_unshift($params, '$this');
2126
                $params = self::implode_r($params);
2127
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2128
                    $callback = $this->customPlugins[$func]['callback'];
2129
                    if ($callback instanceof Closure) {
2130
                        $output = 'call_user_func($this->getCustomPlugin(\'' . $func . '\'), ' . $params . ')';
2131
                    } else {
2132
                        $output = 'call_user_func(\'' . $callback . '\', ' . $params . ')';
2133
                    }
2134
                } else {
2135
                    // Custom plugin
2136
                    if (function_exists('Plugin' . Core::toCamelCase($func)) !== false) {
2137
                        $output = 'Plugin' . Core::toCamelCase($func) . '(' . $params .
2138
                            ')';
2139
                    } // Builtin helper plugin
2140 View Code Duplication
                    elseif(function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func)) !==
2141
                        false) {
2142
                        $output = Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) . '(' .
2143
                            $params . ')';
2144
                    } // Builtin function plugin
2145 View Code Duplication
                    else {
2146
                        $output = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) . '(' .
2147
                            $params . ')';
2148
                    }
2149
                }
2150
            }
2151
        } // Proxy plugin
2152
        elseif ($pluginType & Core::PROXY_PLUGIN) {
2153
            $output = call_user_func(array($this->getCore()->getPluginProxy(), 'getCode'), $func, $params);
2154
        } // Smarty function (@deprecated)
2155
        elseif ($pluginType & Core::SMARTY_FUNCTION) {
2156
            $params = '';
2157
            if (isset($params['*'])) {
2158
                $params = self::implode_r($params['*'], true);
2159
            }
2160
2161
            if ($pluginType & Core::CUSTOM_PLUGIN) {
2162
                $callback = $this->customPlugins[$func]['callback'];
2163
                if (is_array($callback)) {
2164
                    if (is_object($callback[0])) {
2165
                        $output = 'call_user_func_array(array($this->getCustomPlugin(\'' . $func . '\'), \'' . $callback[1] . '\'), array(array(' . $params . '), $this))';
2166 View Code Duplication
                    } else {
2167
                        $output = 'call_user_func_array(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), array(array(' . $params . '), $this))';
2168
                    }
2169
                } else {
2170
                    $output = $callback . '(array(' . $params . '), $this)';
2171
                }
2172
            } else {
2173
                $output = 'smarty_function_' . $func . '(array(' . $params . '), $this)';
2174
            }
2175
        } // Template plugin
2176
        elseif ($pluginType & Core::TEMPLATE_PLUGIN) {
2177
            array_unshift($params, '$this');
2178
            $params                                 = self::implode_r($params);
2179
            $output                                 = 'Plugin' . Core::toCamelCase($func) .
2180
                $this->templatePlugins[$func]['uuid'] . '(' . $params . ')';
2181
            $this->templatePlugins[$func]['called'] = true;
2182
        }
2183
2184 View Code Duplication
        if (is_array($parsingParams)) {
2185
            $parsingParams[] = array($output, $output);
2186
2187
            return $parsingParams;
2188
        } elseif ($curBlock === 'namedparam') {
2189
            return array($output, $output);
2190
        }
2191
2192
        return $output;
2193
    }
2194
2195
    /**
2196
     * Parses a string.
2197
     *
2198
     * @param string $in            the string within which we must parse something
2199
     * @param int    $from          the starting offset of the parsed area
2200
     * @param int    $to            the ending offset of the parsed area
2201
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2202
     *                              default
2203
     * @param string $curBlock      the current parser-block being processed
2204
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2205
     *                              or null by default
2206
     *
2207
     * @return string parsed values
2208
     * @throws CompilationException
2209
     */
2210
    protected function parseString($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2211
    {
2212
        $substr = substr($in, $from, $to - $from);
2213
        $first  = $substr[0];
2214
2215
        if ($this->debug) {
2216
            echo 'STRING FOUND (in ' . htmlentities(substr($in, $from, min($to - $from, 50))) . (($to - $from) > 50 ? '...' : '') . ')' . "\n";
2217
        }
2218
        $strend = false;
2219
        $o      = $from + 1;
2220
        while ($strend === false) {
2221
            $strend = strpos($in, $first, $o);
2222 View Code Duplication
            if ($strend === false) {
2223
                throw new CompilationException($this, 'Unfinished string, started with ' . substr($in, $from, $to - $from));
2224
            }
2225
            if (substr($in, $strend - 1, 1) === '\\') {
2226
                $o      = $strend + 1;
2227
                $strend = false;
2228
            }
2229
        }
2230
        if ($this->debug) {
2231
            echo 'STRING DELIMITED: ' . substr($in, $from, $strend + 1 - $from) . "\n";
2232
        }
2233
2234
        $srcOutput = substr($in, $from, $strend + 1 - $from);
2235
2236
        if ($pointer !== null) {
2237
            $pointer += strlen($srcOutput);
2238
        }
2239
2240
        $output = $this->replaceStringVars($srcOutput, $first);
2241
2242
        // handle modifiers
2243
        if ($curBlock !== 'modifier' && preg_match('#^((?:\|(?:@?[a-z0-9_]+(?::.*)*))+)#i', substr($substr, $strend + 1 - $from), $match)) {
2244
            $modstr = $match[1];
2245
2246
            if ($curBlock === 'root' && substr($modstr, - 1) === '}') {
2247
                $modstr = substr($modstr, 0, - 1);
2248
            }
2249
            $modstr = str_replace('\\' . $first, $first, $modstr);
2250
            $ptr    = 0;
2251
            $output = $this->replaceModifiers(array(null, null, $output, $modstr), 'string', $ptr);
2252
2253
            $strend += $ptr;
2254
            if ($pointer !== null) {
2255
                $pointer += $ptr;
2256
            }
2257
            $srcOutput .= substr($substr, $strend + 1 - $from, $ptr);
2258
        }
2259
2260
        if (is_array($parsingParams)) {
2261
            $parsingParams[] = array($output, substr($srcOutput, 1, - 1));
2262
2263
            return $parsingParams;
2264
        } elseif ($curBlock === 'namedparam') {
2265
            return array($output, substr($srcOutput, 1, - 1));
2266
        }
2267
2268
        return $output;
2269
    }
2270
2271
    /**
2272
     * Parses a constant.
2273
     *
2274
     * @param string $in            the string within which we must parse something
2275
     * @param int    $from          the starting offset of the parsed area
2276
     * @param int    $to            the ending offset of the parsed area
2277
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2278
     *                              default
2279
     * @param string $curBlock      the current parser-block being processed
2280
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2281
     *                              or null by default
2282
     *
2283
     * @return string parsed values
2284
     * @throws CompilationException
2285
     */
2286
    protected function parseConst($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2287
    {
2288
        $substr = substr($in, $from, $to - $from);
2289
2290
        if ($this->debug) {
2291
            echo 'CONST FOUND : ' . $substr . "\n";
2292
        }
2293
2294
        if (!preg_match('#^%([\\\\a-z0-9_:]+)#i', $substr, $m)) {
2295
            throw new CompilationException($this, 'Invalid constant');
2296
        }
2297
2298
        if ($pointer !== null) {
2299
            $pointer += strlen($m[0]);
2300
        }
2301
2302
        $output = $this->parseConstKey($m[1], $curBlock);
2303
2304 View Code Duplication
        if (is_array($parsingParams)) {
2305
            $parsingParams[] = array($output, $m[1]);
2306
2307
            return $parsingParams;
2308
        } elseif ($curBlock === 'namedparam') {
2309
            return array($output, $m[1]);
2310
        }
2311
2312
        return $output;
2313
    }
2314
2315
    /**
2316
     * Parses a constant.
2317
     *
2318
     * @param string $key      the constant to parse
2319
     * @param string $curBlock the current parser-block being processed
2320
     *
2321
     * @return string parsed constant
2322
     */
2323
    protected function parseConstKey($key, $curBlock)
2324
    {
2325
        $key = str_replace('\\\\', '\\', $key);
2326
2327
        if ($this->securityPolicy !== null && $this->securityPolicy->getConstantHandling() === SecurityPolicy::CONST_DISALLOW) {
2328
            return 'null';
2329
        }
2330
2331
        if ($curBlock !== 'root') {
2332
            return '(defined("' . $key . '") ? ' . $key . ' : null)';
2333
        }
2334
2335
        return $key;
2336
    }
2337
2338
    /**
2339
     * Parses a variable.
2340
     *
2341
     * @param string $in            the string within which we must parse something
2342
     * @param int    $from          the starting offset of the parsed area
2343
     * @param int    $to            the ending offset of the parsed area
2344
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2345
     *                              default
2346
     * @param string $curBlock      the current parser-block being processed
2347
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2348
     *                              or null by default
2349
     *
2350
     * @return string parsed values
2351
     * @throws CompilationException
2352
     */
2353
    protected function parseVar($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2354
    {
2355
        $substr = substr($in, $from, $to - $from);
2356
2357
        // var key
2358
        $varRegex = '(\\$?\\.?[a-z0-9\\\\_:]*(?:(?:(?:\\.|->)(?:[a-z0-9\\\\_:]+|(?R))|\\[(?:[a-z0-9\\\\_:]+|(?R)|(["\'])[^\\2]*?\\2)\\]))*)';
2359
        // method call
2360
        $methodCall = ($curBlock === 'root' || $curBlock === 'function' || $curBlock === 'namedparam' || $curBlock === 'condition' || $curBlock === 'variable' || $curBlock === 'expression' || $curBlock === 'delimited_string' ? '(\(.*)?' : '()');
2361
        // simple math expressions
2362
        $simpleMathExpressions = ($curBlock === 'root' || $curBlock === 'function' || $curBlock === 'namedparam' || $curBlock === 'condition' || $curBlock === 'variable' || $curBlock === 'delimited_string' ? '((?:(?:[+\/*%=-])(?:(?<!=)=?-?[$%][a-z0-9\\\\.[\]>_:-]+(?:\([^)]*\))?|(?<!=)=?-?[0-9\.,]*|[+-]))*)' : '()');
2363
        // modifiers
2364
        $modifiers = $curBlock !== 'modifier' ? '((?:\|(?:@?[a-z0-9\\\\_]+(?:(?::("|\').*?\5|:[^`]*))*))+)?' : '(())';
2365
2366
        $regex = '#';
2367
        $regex .= $varRegex;
2368
        $regex .= $methodCall;
2369
        $regex .= $simpleMathExpressions;
2370
        $regex .= $modifiers;
2371
        $regex .= '#i';
2372
2373
        if (preg_match($regex, $substr, $match)) {
2374
            $key = substr($match[1], 1);
2375
2376
            $matchedLength = strlen($match[0]);
2377
            $hasModifiers  = !empty($match[5]);
2378
            $hasExpression = !empty($match[4]);
2379
            $hasMethodCall = !empty($match[3]);
2380
2381
            if (substr($key, - 1) == '.') {
2382
                $key = substr($key, 0, - 1);
2383
                -- $matchedLength;
2384
            }
2385
2386
            if ($hasMethodCall) {
2387
                $matchedLength -= strlen($match[3]) + strlen(substr($match[1], strrpos($match[1], '->')));
2388
                $key        = substr($match[1], 1, strrpos($match[1], '->') - 1);
2389
                $methodCall = substr($match[1], strrpos($match[1], '->')) . $match[3];
2390
            }
2391
2392
            if ($hasModifiers) {
2393
                $matchedLength -= strlen($match[5]);
2394
            }
2395
2396
            if ($pointer !== null) {
2397
                $pointer += $matchedLength;
2398
            }
2399
2400
            // replace useless brackets by dot accessed vars and strip enclosing quotes if present
2401
            $key = preg_replace('#\[(["\']?)([^$%\[.>-]+)\1\]#', '.$2', $key);
2402
2403 View Code Duplication
            if ($this->debug) {
2404
                if ($hasMethodCall) {
2405
                    echo 'METHOD CALL FOUND : $' . $key . substr($methodCall, 0, 30) . "\n";
2406
                } else {
2407
                    echo 'VAR FOUND : $' . $key . "\n";
2408
                }
2409
            }
2410
2411
            $key = str_replace('"', '\\"', $key);
2412
2413
            $cnt = substr_count($key, '$');
2414
            if ($cnt > 0) {
2415
                $uid           = 0;
2416
                $parsed        = array($uid => '');
2417
                $current       = &$parsed;
2418
                $curTxt        = &$parsed[$uid ++];
2419
                $tree          = array();
2420
                $chars         = str_split($key, 1);
2421
                $inSplittedVar = false;
2422
                $bracketCount  = 0;
2423
2424
                while (($char = array_shift($chars)) !== null) {
2425
                    if ($char === '[') {
2426
                        if (count($tree) > 0) {
2427
                            ++ $bracketCount;
2428
                        } else {
2429
                            $tree[]        = &$current;
2430
                            $current[$uid] = array($uid + 1 => '');
2431
                            $current       = &$current[$uid ++];
2432
                            $curTxt        = &$current[$uid ++];
2433
                            continue;
2434
                        }
2435
                    } elseif ($char === ']') {
2436
                        if ($bracketCount > 0) {
2437
                            -- $bracketCount;
2438
                        } else {
2439
                            $current = &$tree[count($tree) - 1];
2440
                            array_pop($tree);
2441
                            if (current($chars) !== '[' && current($chars) !== false && current($chars) !== ']') {
2442
                                $current[$uid] = '';
2443
                                $curTxt        = &$current[$uid ++];
2444
                            }
2445
                            continue;
2446
                        }
2447
                    } elseif ($char === '$') {
2448
                        if (count($tree) == 0) {
2449
                            $curTxt        = &$current[$uid ++];
2450
                            $inSplittedVar = true;
2451
                        }
2452
                    } elseif (($char === '.' || $char === '-') && count($tree) == 0 && $inSplittedVar) {
2453
                        $curTxt        = &$current[$uid ++];
2454
                        $inSplittedVar = false;
2455
                    }
2456
2457
                    $curTxt .= $char;
2458
                }
2459
                unset($uid, $current, $curTxt, $tree, $chars);
2460
2461
                if ($this->debug) {
2462
                    echo 'RECURSIVE VAR REPLACEMENT : ' . $key . "\n";
2463
                }
2464
2465
                $key = $this->flattenVarTree($parsed);
2466
2467
                if ($this->debug) {
2468
                    echo 'RECURSIVE VAR REPLACEMENT DONE : ' . $key . "\n";
2469
                }
2470
2471
                $output = preg_replace('#(^""\.|""\.|\.""$|(\()""\.|\.""(\)))#', '$2$3', '$this->readVar("' . $key . '")');
2472
            } else {
2473
                $output = $this->parseVarKey($key, $hasModifiers ? 'modifier' : $curBlock);
2474
            }
2475
2476
2477
            // methods
2478
            if ($hasMethodCall) {
2479
                $ptr = 0;
2480
2481
                $output = $this->parseMethodCall($output, $methodCall, $curBlock, $ptr);
2482
2483
                if ($pointer !== null) {
2484
                    $pointer += $ptr;
2485
                }
2486
                $matchedLength += $ptr;
2487
            }
2488
2489
            if ($hasExpression) {
2490
                // expressions
2491
                preg_match_all('#(?:([+/*%=-])(=?-?[%$][a-z0-9\\\\.[\]>_:-]+(?:\([^)]*\))?|=?-?[0-9.,]+|\1))#i', $match[4], $expMatch);
2492
                foreach ($expMatch[1] as $k => $operator) {
2493
                    if (substr($expMatch[2][$k], 0, 1) === '=') {
2494
                        $assign = true;
2495
                        if ($operator === '=') {
2496
                            throw new CompilationException($this, 'Invalid expression <em>' . $substr . '</em>, can not use "==" in expressions');
2497
                        }
2498
                        if ($curBlock !== 'root') {
2499
                            throw new CompilationException($this, 'Invalid expression <em>' . $substr . '</em>, assignments can only be used in top level expressions like {$foo+=3} or {$foo="bar"}');
2500
                        }
2501
                        $operator .= '=';
2502
                        $expMatch[2][$k] = substr($expMatch[2][$k], 1);
2503
                    }
2504
2505
                    if (substr($expMatch[2][$k], 0, 1) === '-' && strlen($expMatch[2][$k]) > 1) {
2506
                        $operator .= '-';
2507
                        $expMatch[2][$k] = substr($expMatch[2][$k], 1);
2508
                    }
2509
                    if (($operator === '+' || $operator === '-') && $expMatch[2][$k] === $operator) {
2510
                        $output = '(' . $output . $operator . $operator . ')';
2511
                        break;
2512
                    } elseif (substr($expMatch[2][$k], 0, 1) === '$') {
2513
                        $output = '(' . $output . ' ' . $operator . ' ' . $this->parseVar($expMatch[2][$k], 0, strlen($expMatch[2][$k]), false, 'expression') . ')';
2514
                    } elseif (substr($expMatch[2][$k], 0, 1) === '%') {
2515
                        $output = '(' . $output . ' ' . $operator . ' ' . $this->parseConst($expMatch[2][$k], 0, strlen($expMatch[2][$k]), false, 'expression') . ')';
2516
                    } elseif (!empty($expMatch[2][$k])) {
2517
                        $output = '(' . $output . ' ' . $operator . ' ' . str_replace(',', '.', $expMatch[2][$k]) . ')';
2518
                    } else {
2519
                        throw new CompilationException($this, 'Unfinished expression <em>' . $substr . '</em>, missing var or number after math operator');
2520
                    }
2521
                }
2522
            }
2523
2524
            if ($this->autoEscape === true && $curBlock !== 'condition') {
2525
                $output = '(is_string($tmp=' . $output . ') ? htmlspecialchars($tmp, ENT_QUOTES, $this->charset) : $tmp)';
2526
            }
2527
2528
            // handle modifiers
2529
            if ($curBlock !== 'modifier' && $hasModifiers) {
2530
                $ptr    = 0;
2531
                $output = $this->replaceModifiers(array(null, null, $output, $match[5]), 'var', $ptr);
2532
                if ($pointer !== null) {
2533
                    $pointer += $ptr;
2534
                }
2535
                $matchedLength += $ptr;
2536
            }
2537
2538
            if (is_array($parsingParams)) {
2539
                $parsingParams[] = array($output, $key);
2540
2541
                return $parsingParams;
2542
            } elseif ($curBlock === 'namedparam') {
2543
                return array($output, $key);
2544
            } elseif ($curBlock === 'string' || $curBlock === 'delimited_string') {
2545
                return array($matchedLength, $output);
2546
            } elseif ($curBlock === 'expression' || $curBlock === 'variable') {
2547
                return $output;
2548
            } elseif (isset($assign)) {
2549
                return self::PHP_OPEN . $output . ';' . self::PHP_CLOSE;
2550
            }
2551
2552
            return $output;
2553
        } else {
2554
            if ($curBlock === 'string' || $curBlock === 'delimited_string') {
2555
                return array(0, '');
2556
            }
2557
            throw new CompilationException($this, 'Invalid variable name <em>' . $substr . '</em>');
2558
        }
2559
    }
2560
2561
    /**
2562
     * Parses any number of chained method calls/property reads.
2563
     *
2564
     * @param string $output     the variable or whatever upon which the method are called
2565
     * @param string $methodCall method call source, starting at "->"
2566
     * @param string $curBlock   the current parser-block being processed
2567
     * @param int    $pointer    a reference to a pointer that will be increased by the amount of characters parsed
2568
     *
2569
     * @return string parsed call(s)/read(s)
2570
     */
2571
    protected function parseMethodCall($output, $methodCall, $curBlock, &$pointer)
2572
    {
2573
        $ptr = 0;
2574
        $len = strlen($methodCall);
2575
2576
        while ($ptr < $len) {
2577
            if (strpos($methodCall, '->', $ptr) === $ptr) {
2578
                $ptr += 2;
2579
            }
2580
2581
            if (in_array(
2582
                $methodCall[$ptr], array(
2583
                    ';',
2584
                    ',',
2585
                    '/',
2586
                    ' ',
2587
                    "\t",
2588
                    "\r",
2589
                    "\n",
2590
                    ')',
2591
                    '+',
2592
                    '*',
2593
                    '%',
2594
                    '=',
2595
                    '-',
2596
                    '|'
2597
                )
2598
            ) || substr($methodCall, $ptr, strlen($this->rd)) === $this->rd
2599
            ) {
2600
                // break char found
2601
                break;
2602
            }
2603
2604
            if (!preg_match('/^([a-z0-9_]+)(\(.*?\))?/i', substr($methodCall, $ptr), $methMatch)) {
2605
                break;
2606
            }
2607
2608
            if (empty($methMatch[2])) {
2609
                // property
2610
                if ($curBlock === 'root') {
2611
                    $output .= '->' . $methMatch[1];
2612
                } else {
2613
                    $output = '(($tmp = ' . $output . ') ? $tmp->' . $methMatch[1] . ' : null)';
2614
                }
2615
                $ptr += strlen($methMatch[1]);
2616
            } else {
2617
                // method
2618
                if (substr($methMatch[2], 0, 2) === '()') {
2619
                    $parsedCall = $methMatch[1] . '()';
2620
                    $ptr += strlen($methMatch[1]) + 2;
2621
                } else {
2622
                    $parsedCall = $this->parseFunction($methodCall, $ptr, strlen($methodCall), false, 'method', $ptr);
2623
                }
2624
                if ($this->securityPolicy !== null) {
2625
                    $argPos = strpos($parsedCall, '(');
2626
                    $method = strtolower(substr($parsedCall, 0, $argPos));
2627
                    $args   = substr($parsedCall, $argPos);
2628
                    if ($curBlock === 'root') {
2629
                        $output = '$this->getSecurityPolicy()->callMethod($this, ' . $output . ', ' . var_export($method, true) . ', array' . $args . ')';
2630
                    } else {
2631
                        $output = '(($tmp = ' . $output . ') ? $this->getSecurityPolicy()->callMethod($this, $tmp, ' . var_export($method, true) . ', array' . $args . ') : null)';
2632
                    }
2633 View Code Duplication
                } else {
2634
                    if ($curBlock === 'root') {
2635
                        $output .= '->' . $parsedCall;
2636
                    } else {
2637
                        $output = '(($tmp = ' . $output . ') ? $tmp->' . $parsedCall . ' : null)';
2638
                    }
2639
                }
2640
            }
2641
        }
2642
2643
        $pointer += $ptr;
2644
2645
        return $output;
2646
    }
2647
2648
    /**
2649
     * Parses a constant variable (a variable that doesn't contain another variable) and preprocesses it to save
2650
     * runtime processing time.
2651
     *
2652
     * @param string $key      the variable to parse
2653
     * @param string $curBlock the current parser-block being processed
2654
     *
2655
     * @return string parsed variable
2656
     */
2657
    protected function parseVarKey($key, $curBlock)
2658
    {
2659
        if ($key === '') {
2660
            return '$this->scope';
2661
        }
2662
        if (substr($key, 0, 1) === '.') {
2663
            $key = 'dwoo' . $key;
2664
        }
2665
        if (preg_match('#dwoo\.(get|post|server|cookies|session|env|request)((?:\.[a-z0-9_-]+)+)#i', $key, $m)) {
2666
            $global = strtoupper($m[1]);
2667
            if ($global === 'COOKIES') {
2668
                $global = 'COOKIE';
2669
            }
2670
            $key = '$_' . $global;
2671
            foreach (explode('.', ltrim($m[2], '.')) as $part) {
2672
                $key .= '[' . var_export($part, true) . ']';
2673
            }
2674
            if ($curBlock === 'root') {
2675
                $output = $key;
2676
            } else {
2677
                $output = '(isset(' . $key . ')?' . $key . ':null)';
2678
            }
2679
        } elseif (preg_match('#dwoo\\.const\\.([a-z0-9\\\\_:]+)#i', $key, $m)) {
2680
            return $this->parseConstKey($m[1], $curBlock);
2681
        } elseif ($this->scope !== null) {
2682
            if (strstr($key, '.') === false && strstr($key, '[') === false && strstr($key, '->') === false) {
2683
                if ($key === 'dwoo') {
2684
                    $output = '$this->globals';
2685
                } elseif ($key === '_root' || $key === '__') {
2686
                    $output = '$this->data';
2687
                } elseif ($key === '_parent' || $key === '_') {
2688
                    $output = '$this->readParentVar(1)';
2689
                } elseif ($key === '_key') {
2690
                    $output = '$tmp_key';
2691 View Code Duplication
                } else {
2692
                    if ($curBlock === 'root') {
2693
                        $output = '$this->scope["' . $key . '"]';
2694
                    } else {
2695
                        $output = '(isset($this->scope["' . $key . '"]) ? $this->scope["' . $key . '"] : null)';
2696
                    }
2697
                }
2698
            } else {
2699
                preg_match_all('#(\[|->|\.)?((?:[a-z0-9_]|-(?!>))+|(\\\?[\'"])[^\3]*?\3)\]?#i', $key, $m);
2700
2701
                $i = $m[2][0];
2702
                if ($i === '_parent' || $i === '_') {
2703
                    $parentCnt = 0;
2704
2705
                    while (true) {
2706
                        ++ $parentCnt;
2707
                        array_shift($m[2]);
2708
                        array_shift($m[1]);
2709
                        if (current($m[2]) === '_parent') {
2710
                            continue;
2711
                        }
2712
                        break;
2713
                    }
2714
2715
                    $output = '$this->readParentVar(' . $parentCnt . ')';
2716
                } else {
2717
                    if ($i === 'dwoo') {
2718
                        $output = '$this->globals';
2719
                        array_shift($m[2]);
2720
                        array_shift($m[1]);
2721 View Code Duplication
                    } elseif ($i === '_root' || $i === '__') {
2722
                        $output = '$this->data';
2723
                        array_shift($m[2]);
2724
                        array_shift($m[1]);
2725
                    } elseif ($i === '_key') {
2726
                        $output = '$tmp_key';
2727
                    } else {
2728
                        $output = '$this->scope';
2729
                    }
2730
2731
                    while (count($m[1]) && $m[1][0] !== '->') {
2732
                        $m[2][0] = preg_replace('/(^\\\([\'"])|\\\([\'"])$)/x', '$2$3', $m[2][0]);
2733
                        if (substr($m[2][0], 0, 1) == '"' || substr($m[2][0], 0, 1) == "'") {
2734
                            $output .= '[' . $m[2][0] . ']';
2735
                        } else {
2736
                            $output .= '["' . $m[2][0] . '"]';
2737
                        }
2738
                        array_shift($m[2]);
2739
                        array_shift($m[1]);
2740
                    }
2741
2742
                    if ($curBlock !== 'root') {
2743
                        $output = '(isset(' . $output . ') ? ' . $output . ':null)';
2744
                    }
2745
                }
2746
2747
                if (count($m[2])) {
2748
                    unset($m[0]);
2749
                    $output = '$this->readVarInto(' . str_replace("\n", '', var_export($m, true)) . ', ' . $output . ', ' . ($curBlock == 'root' ? 'false' : 'true') . ')';
2750
                }
2751
            }
2752
        } else {
2753
            preg_match_all('#(\[|->|\.)?((?:[a-z0-9_]|-(?!>))+)\]?#i', $key, $m);
2754
            unset($m[0]);
2755
            $output = '$this->readVar(' . str_replace("\n", '', var_export($m, true)) . ')';
2756
        }
2757
2758
        return $output;
2759
    }
2760
2761
    /**
2762
     * Flattens a variable tree, this helps in parsing very complex variables such as $var.foo[$foo.bar->baz].baz,
2763
     * it computes the contents of the brackets first and works out from there.
2764
     *
2765
     * @param array $tree     the variable tree parsed by he parseVar() method that must be flattened
2766
     * @param bool  $recursed leave that to false by default, it is only for internal use
2767
     *
2768
     * @return string flattened tree
2769
     */
2770
    protected function flattenVarTree(array $tree, $recursed = false)
2771
    {
2772
        $out = $recursed ? '".$this->readVarInto(' : '';
2773
        foreach ($tree as $bit) {
2774
            if (is_array($bit)) {
2775
                $out .= '.' . $this->flattenVarTree($bit, false);
2776
            } else {
2777
                $key = str_replace('"', '\\"', $bit);
2778
2779
                if (substr($key, 0, 1) === '$') {
2780
                    $out .= '".' . $this->parseVar($key, 0, strlen($key), false, 'variable') . '."';
2781
                } else {
2782
                    $cnt = substr_count($key, '$');
2783
2784
                    if ($this->debug) {
2785
                        echo 'PARSING SUBVARS IN : ' . $key . "\n";
2786
                    }
2787
                    if ($cnt > 0) {
2788
                        while (-- $cnt >= 0) {
2789
                            if (isset($last)) {
2790
                                $last = strrpos($key, '$', - (strlen($key) - $last + 1));
2791
                            } else {
2792
                                $last = strrpos($key, '$');
2793
                            }
2794
                            preg_match('#\$[a-z0-9_]+((?:(?:\.|->)(?:[a-z0-9_]+|(?R))|\[(?:[a-z0-9_]+|(?R))\]))*' . '((?:(?:[+/*%-])(?:\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))*)#i', substr($key, $last), $submatch);
2795
2796
                            $len = strlen($submatch[0]);
2797
                            $key = substr_replace(
2798
                                $key, preg_replace_callback(
2799
                                    '#(\$[a-z0-9_]+((?:(?:\.|->)(?:[a-z0-9_]+|(?R))|\[(?:[a-z0-9_]+|(?R))\]))*)' . '((?:(?:[+/*%-])(?:\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))*)#i', array(
2800
                                        $this,
2801
                                        'replaceVarKeyHelper'
2802
                                    ), substr($key, $last, $len)
2803
                                ), $last, $len
2804
                            );
2805
                            if ($this->debug) {
2806
                                echo 'RECURSIVE VAR REPLACEMENT DONE : ' . $key . "\n";
2807
                            }
2808
                        }
2809
                        unset($last);
2810
2811
                        $out .= $key;
2812
                    } else {
2813
                        $out .= $key;
2814
                    }
2815
                }
2816
            }
2817
        }
2818
        $out .= $recursed ? ', true)."' : '';
2819
2820
        return $out;
2821
    }
2822
2823
    /**
2824
     * Helper function that parses a variable.
2825
     *
2826
     * @param array $match the matched variable, array(1=>"string match")
2827
     *
2828
     * @return string parsed variable
2829
     */
2830
    protected function replaceVarKeyHelper($match)
2831
    {
2832
        return '".' . $this->parseVar($match[0], 0, strlen($match[0]), false, 'variable') . '."';
2833
    }
2834
2835
    /**
2836
     * Parses various constants, operators or non-quoted strings.
2837
     *
2838
     * @param string $in            the string within which we must parse something
2839
     * @param int    $from          the starting offset of the parsed area
2840
     * @param int    $to            the ending offset of the parsed area
2841
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2842
     *                              default
2843
     * @param string $curBlock      the current parser-block being processed
2844
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2845
     *                              or null by default
2846
     *
2847
     * @return string parsed values
2848
     * @throws Exception
2849
     */
2850
    protected function parseOthers($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2851
    {
2852
        $substr = substr($in, $from, $to - $from);
2853
2854
        $end = strlen($substr);
2855
2856
        if ($curBlock === 'condition') {
2857
            $breakChars = array(
2858
                '(',
2859
                ')',
2860
                ' ',
2861
                '||',
2862
                '&&',
2863
                '|',
2864
                '&',
2865
                '>=',
2866
                '<=',
2867
                '===',
2868
                '==',
2869
                '=',
2870
                '!==',
2871
                '!=',
2872
                '<<',
2873
                '<',
2874
                '>>',
2875
                '>',
2876
                '^',
2877
                '~',
2878
                ',',
2879
                '+',
2880
                '-',
2881
                '*',
2882
                '/',
2883
                '%',
2884
                '!',
2885
                '?',
2886
                ':',
2887
                $this->rd,
2888
                ';'
2889
            );
2890
        } elseif ($curBlock === 'modifier') {
2891
            $breakChars = array(' ', ',', ')', ':', '|', "\r", "\n", "\t", ';', $this->rd);
2892
        } elseif ($curBlock === 'expression') {
2893
            $breakChars = array('/', '%', '+', '-', '*', ' ', ',', ')', "\r", "\n", "\t", ';', $this->rd);
2894
        } else {
2895
            $breakChars = array(' ', ',', ')', "\r", "\n", "\t", ';', $this->rd);
2896
        }
2897
2898
        $breaker = false;
2899
        foreach ($breakChars as $k => $char) {
2900
            $test = strpos($substr, $char);
2901
            if ($test !== false && $test < $end) {
2902
                $end     = $test;
2903
                $breaker = $k;
2904
            }
2905
        }
2906
2907
        if ($curBlock === 'condition') {
2908
            if ($end === 0 && $breaker !== false) {
2909
                $end = strlen($breakChars[$breaker]);
2910
            }
2911
        }
2912
2913
        if ($end !== false) {
2914
            $substr = substr($substr, 0, $end);
2915
        }
2916
2917
        if ($pointer !== null) {
2918
            $pointer += strlen($substr);
2919
        }
2920
2921
        $src    = $substr;
2922
        $substr = trim($substr);
2923
2924
        if (strtolower($substr) === 'false' || strtolower($substr) === 'no' || strtolower($substr) === 'off') {
2925
            if ($this->debug) {
2926
                echo 'BOOLEAN(FALSE) PARSED' . "\n";
2927
            }
2928
            $substr = 'false';
2929
            $type   = self::T_BOOL;
2930
        } elseif (strtolower($substr) === 'true' || strtolower($substr) === 'yes' || strtolower($substr) === 'on') {
2931
            if ($this->debug) {
2932
                echo 'BOOLEAN(TRUE) PARSED' . "\n";
2933
            }
2934
            $substr = 'true';
2935
            $type   = self::T_BOOL;
2936 View Code Duplication
        } elseif ($substr === 'null' || $substr === 'NULL') {
2937
            if ($this->debug) {
2938
                echo 'NULL PARSED' . "\n";
2939
            }
2940
            $substr = 'null';
2941
            $type   = self::T_NULL;
2942
        } elseif (is_numeric($substr)) {
2943
            $substr = (float)$substr;
2944
            if ((int)$substr == $substr) {
2945
                $substr = (int)$substr;
2946
            }
2947
            $type = self::T_NUMERIC;
2948
            if ($this->debug) {
2949
                echo 'NUMBER (' . $substr . ') PARSED' . "\n";
2950
            }
2951 View Code Duplication
        } elseif (preg_match('{^-?(\d+|\d*(\.\d+))\s*([/*%+-]\s*-?(\d+|\d*(\.\d+)))+$}', $substr)) {
2952
            if ($this->debug) {
2953
                echo 'SIMPLE MATH PARSED . "\n"';
2954
            }
2955
            $type   = self::T_MATH;
2956
            $substr = '(' . $substr . ')';
2957
        } elseif ($curBlock === 'condition' && array_search($substr, $breakChars, true) !== false) {
2958
            if ($this->debug) {
2959
                echo 'BREAKCHAR (' . $substr . ') PARSED' . "\n";
2960
            }
2961
            $type = self::T_BREAKCHAR;
2962
            //$substr = '"'.$substr.'"';
2963
        } else {
2964
            $substr = $this->replaceStringVars('\'' . str_replace('\'', '\\\'', $substr) . '\'', '\'', $curBlock);
2965
            $type   = self::T_UNQUOTED_STRING;
2966
            if ($this->debug) {
2967
                echo 'BLABBER (' . $substr . ') CASTED AS STRING' . "\n";
2968
            }
2969
        }
2970
2971
        if (is_array($parsingParams)) {
2972
            $parsingParams[] = array($substr, $src, $type);
2973
2974
            return $parsingParams;
2975
        } elseif ($curBlock === 'namedparam') {
2976
            return array($substr, $src, $type);
2977
        } elseif ($curBlock === 'expression') {
2978
            return $substr;
2979
        } else {
2980
            throw new Exception('Something went wrong');
2981
        }
2982
    }
2983
2984
    /**
2985
     * Replaces variables within a parsed string.
2986
     *
2987
     * @param string $string   the parsed string
2988
     * @param string $first    the first character parsed in the string, which is the string delimiter (' or ")
2989
     * @param string $curBlock the current parser-block being processed
2990
     *
2991
     * @return string the original string with variables replaced
2992
     */
2993
    protected function replaceStringVars($string, $first, $curBlock = '')
2994
    {
2995
        $pos = 0;
2996
        if ($this->debug) {
2997
            echo 'STRING VAR REPLACEMENT : ' . $string . "\n";
2998
        }
2999
        // replace vars
3000
        while (($pos = strpos($string, '$', $pos)) !== false) {
3001
            $prev = substr($string, $pos - 1, 1);
3002
            if ($prev === '\\') {
3003
                ++ $pos;
3004
                continue;
3005
            }
3006
3007
            $var = $this->parse($string, $pos, null, false, ($curBlock === 'modifier' ? 'modifier' : ($prev === '`' ? 'delimited_string' : 'string')));
3008
            $len = $var[0];
3009
            $var = $this->parse(str_replace('\\' . $first, $first, $string), $pos, null, false, ($curBlock === 'modifier' ? 'modifier' : ($prev === '`' ? 'delimited_string' : 'string')));
3010
3011
            if ($prev === '`' && substr($string, $pos + $len, 1) === '`') {
3012
                $string = substr_replace($string, $first . '.' . $var[1] . '.' . $first, $pos - 1, $len + 2);
3013
            } else {
3014
                $string = substr_replace($string, $first . '.' . $var[1] . '.' . $first, $pos, $len);
3015
            }
3016
            $pos += strlen($var[1]) + 2;
3017
            if ($this->debug) {
3018
                echo 'STRING VAR REPLACEMENT DONE : ' . $string . "\n";
3019
            }
3020
        }
3021
3022
        // handle modifiers
3023
        // TODO Obsolete?
3024
        $string = preg_replace_callback(
3025
            '#("|\')\.(.+?)\.\1((?:\|(?:@?[a-z0-9_]+(?:(?::("|\').+?\4|:[^`]*))*))+)#i', array(
3026
            $this,
3027
            'replaceModifiers'
3028
            ), $string
3029
        );
3030
3031
        // replace escaped dollar operators by unescaped ones if required
3032
        if ($first === "'") {
3033
            $string = str_replace('\\$', '$', $string);
3034
        }
3035
3036
        return $string;
3037
    }
3038
3039
    /**
3040
     * Replaces the modifiers applied to a string or a variable.
3041
     *
3042
     * @param array  $m        the regex matches that must be array(1=>"double or single quotes enclosing a string,
3043
     *                         when applicable", 2=>"the string or var", 3=>"the modifiers matched")
3044
     * @param string $curBlock the current parser-block being processed
3045
     * @param null   $pointer
3046
     *
3047
     * @return string the input enclosed with various function calls according to the modifiers found
3048
     * @throws CompilationException
3049
     * @throws Exception
3050
     */
3051
    protected function replaceModifiers(array $m, $curBlock = null, &$pointer = null)
3052
    {
3053
        if ($this->debug) {
3054
            echo 'PARSING MODIFIERS : ' . $m[3] . "\n";
3055
        }
3056
3057
        if ($pointer !== null) {
3058
            $pointer += strlen($m[3]);
3059
        }
3060
        // remove first pipe
3061
        $cmdstrsrc = substr($m[3], 1);
3062
        // remove last quote if present
3063
        if (substr($cmdstrsrc, - 1, 1) === $m[1]) {
3064
            $cmdstrsrc = substr($cmdstrsrc, 0, - 1);
3065
            $add       = $m[1];
3066
        }
3067
3068
        $output = $m[2];
3069
3070
        $continue = true;
3071
        while (strlen($cmdstrsrc) > 0 && $continue) {
3072
            if ($cmdstrsrc[0] === '|') {
3073
                $cmdstrsrc = substr($cmdstrsrc, 1);
3074
                continue;
3075
            }
3076
            if ($cmdstrsrc[0] === ' ' || $cmdstrsrc[0] === ';' || substr($cmdstrsrc, 0, strlen($this->rd)) === $this->rd) {
3077
                if ($this->debug) {
3078
                    echo 'MODIFIER PARSING ENDED, RIGHT DELIMITER or ";" FOUND' . "\n";
3079
                }
3080
                $continue = false;
3081
                if ($pointer !== null) {
3082
                    $pointer -= strlen($cmdstrsrc);
3083
                }
3084
                break;
3085
            }
3086
            $cmdstr   = $cmdstrsrc;
3087
            $paramsep = ':';
3088 View Code Duplication
            if (!preg_match('/^(@{0,2}[a-z_][a-z0-9_]*)(:)?/i', $cmdstr, $match)) {
3089
                throw new CompilationException($this, 'Invalid modifier name, started with : ' . substr($cmdstr, 0, 10));
3090
            }
3091
            $paramspos = !empty($match[2]) ? strlen($match[1]) : false;
3092
            $func      = $match[1];
3093
3094
            $state = 0;
3095
            if ($paramspos === false) {
3096
                $cmdstrsrc = substr($cmdstrsrc, strlen($func));
3097
                $params    = array();
3098
                if ($this->debug) {
3099
                    echo 'MODIFIER (' . $func . ') CALLED WITH NO PARAMS' . "\n";
3100
                }
3101
            } else {
3102
                $paramstr = substr($cmdstr, $paramspos + 1);
3103 View Code Duplication
                if (substr($paramstr, - 1, 1) === $paramsep) {
3104
                    $paramstr = substr($paramstr, 0, - 1);
3105
                }
3106
3107
                $ptr    = 0;
3108
                $params = array();
3109
                while ($ptr < strlen($paramstr)) {
3110
                    if ($this->debug) {
3111
                        echo 'MODIFIER (' . $func . ') START PARAM PARSING WITH POINTER AT ' . $ptr . "\n";
3112
                    }
3113
                    if ($this->debug) {
3114
                        echo $paramstr . '--' . $ptr . '--' . strlen($paramstr) . '--modifier' . "\n";
3115
                    }
3116
                    $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'modifier', $ptr);
3117
                    if ($this->debug) {
3118
                        echo 'PARAM PARSED, POINTER AT ' . $ptr . "\n";
3119
                    }
3120
3121
                    if ($ptr >= strlen($paramstr)) {
3122
                        if ($this->debug) {
3123
                            echo 'PARAM PARSING ENDED, PARAM STRING CONSUMED' . "\n";
3124
                        }
3125
                        break;
3126
                    }
3127
3128
                    if ($paramstr[$ptr] === ' ' || $paramstr[$ptr] === '|' || $paramstr[$ptr] === ';' || substr($paramstr, $ptr, strlen($this->rd)) === $this->rd) {
3129
                        if ($this->debug) {
3130
                            echo 'PARAM PARSING ENDED, " ", "|", RIGHT DELIMITER or ";" FOUND, POINTER AT ' . $ptr . "\n";
3131
                        }
3132
                        if ($paramstr[$ptr] !== '|') {
3133
                            $continue = false;
3134
                            if ($pointer !== null) {
3135
                                $pointer -= strlen($paramstr) - $ptr;
3136
                            }
3137
                        }
3138
                        ++ $ptr;
3139
                        break;
3140
                    }
3141
                    if ($ptr < strlen($paramstr) && $paramstr[$ptr] === ':') {
3142
                        ++ $ptr;
3143
                    }
3144
                }
3145
                $cmdstrsrc = substr($cmdstrsrc, strlen($func) + 1 + $ptr);
3146
                foreach ($params as $k => $p) {
3147
                    if (is_array($p) && is_array($p[1])) {
3148
                        $state |= 2;
3149
                    } else {
3150
                        if (($state & 2) && preg_match('#^(["\'])(.+?)\1$#', $p[0], $m)) {
3151
                            $params[$k] = array($m[2], array('true', 'true'));
3152
                        } else {
3153
                            if ($state & 2) {
3154
                                throw new CompilationException($this, 'You can not use an unnamed parameter after a named one');
3155
                            }
3156
                            $state |= 1;
3157
                        }
3158
                    }
3159
                }
3160
            }
3161
3162
            // check if we must use array_map with this plugin or not
3163
            $mapped = false;
3164
            if (substr($func, 0, 1) === '@') {
3165
                $func   = substr($func, 1);
3166
                $mapped = true;
3167
            }
3168
3169
            $pluginType = $this->getPluginType($func);
3170
3171
            if ($state & 2) {
3172
                array_unshift($params, array('value', is_array($output) ? $output : array($output, $output)));
3173
            } else {
3174
                array_unshift($params, is_array($output) ? $output : array($output, $output));
3175
            }
3176
3177
            if ($pluginType & Core::NATIVE_PLUGIN) {
3178
                $params = $this->mapParams($params, null, $state);
0 ignored issues
show
null is of type null, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3179
3180
                $params = $params['*'][0];
3181
3182
                $params = self::implode_r($params);
3183
3184 View Code Duplication
                if ($mapped) {
3185
                    $output = '$this->arrayMap(\'' . $func . '\', array(' . $params . '))';
3186
                } else {
3187
                    $output = $func . '(' . $params . ')';
3188
                }
3189
            } elseif ($pluginType & Core::PROXY_PLUGIN) {
3190
                $params = $this->mapParams($params, $this->getCore()->getPluginProxy()->getCallback($func), $state);
3191
                foreach ($params as &$p) {
3192
                    $p = $p[0];
3193
                }
3194
                $output = call_user_func(array($this->getCore()->getPluginProxy(), 'getCode'), $func, $params);
3195
            } elseif ($pluginType & Core::SMARTY_MODIFIER) {
3196
                $params = $this->mapParams($params, null, $state);
0 ignored issues
show
null is of type null, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3197
                $params = $params['*'][0];
3198
3199
                $params = self::implode_r($params);
3200
3201
                if ($pluginType & Core::CUSTOM_PLUGIN) {
3202
                    $callback = $this->customPlugins[$func]['callback'];
3203
                    if (is_array($callback)) {
3204
                        if (is_object($callback[0])) {
3205
                            $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array($this->getCustomPlugin(\'' . $func . '\'), \'' . $callback[1] . '\'), array(' . $params . '))';
3206 View Code Duplication
                        } else {
3207
                            $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), array(' . $params . '))';
3208
                        }
3209
                    } elseif ($mapped) {
3210
                        $output = '$this->arrayMap(\'' . $callback . '\', array(' . $params . '))';
3211
                    } else {
3212
                        $output = $callback . '(' . $params . ')';
3213
                    }
3214
                } elseif ($mapped) {
3215
                    $output = '$this->arrayMap(\'smarty_modifier_' . $func . '\', array(' . $params . '))';
3216
                } else {
3217
                    $output = 'smarty_modifier_' . $func . '(' . $params . ')';
3218
                }
3219
            } else {
3220
                if ($pluginType & Core::CUSTOM_PLUGIN) {
3221
                    $pluginName = $callback = $this->customPlugins[$func]['callback'];
3222
                    if (($pluginType & Core::CLASS_PLUGIN) && !is_array($callback)) {
3223
                        $pluginName = $this->customPlugins[$func]['callback'];
3224
                        $callback   = array($pluginName, ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process');
3225
                    }
3226
                } else {
3227
                    if (class_exists('Plugin' . Core::toCamelCase($func)) !== false || function_exists('Plugin' .
3228
                            Core::toCamelCase($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : ''))
3229
                        !== false) {
3230
                        $pluginName = 'Plugin' . Core::toCamelCase($func);
3231
                    } else {
3232
                        $pluginName = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func);
3233
                    }
3234
                    if ($pluginType & Core::CLASS_PLUGIN) {
3235
                        $callback = array($pluginName, ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process');
3236
                    } else {
3237
                        $callback = $pluginName . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : '');
3238
                    }
3239
                }
3240
                $params = $this->mapParams($params, $callback, $state);
3241
3242
                foreach ($params as &$p) {
3243
                    $p = $p[0];
3244
                }
3245
3246
                // Only for PHP function, who is not a PHP class
3247
                if ($pluginType & Core::FUNC_PLUGIN && !($pluginType & Core::CLASS_PLUGIN)) {
3248
                    if ($pluginType & Core::COMPILABLE_PLUGIN) {
3249
                        if ($mapped) {
3250
                            throw new CompilationException($this, 'The @ operator can not be used on compiled plugins.');
3251
                        }
3252
                        if ($pluginType & Core::CUSTOM_PLUGIN) {
3253
                            $funcCompiler = $this->customPlugins[$func]['callback'];
3254
                        } else {
3255
                            if (function_exists('Plugin' . Core::toCamelCase($func) . 'Compile') !== false) {
3256
                                $funcCompiler = 'Plugin' . Core::toCamelCase($func) . 'Compile';
3257
                            } else {
3258
                                $funcCompiler = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) .
3259
                                    'Compile';
3260
                            }
3261
                        }
3262
                        array_unshift($params, $this);
3263
                        $output = call_user_func_array($funcCompiler, $params);
3264
                    } else {
3265
                        array_unshift($params, '$this');
3266
3267
                        $params = self::implode_r($params);
3268 View Code Duplication
                        if ($mapped) {
3269
                            $output = '$this->arrayMap(\'' . $pluginName . '\', array(' . $params . '))';
3270
                        } else {
3271
                            $output = $pluginName . '(' . $params . ')';
3272
                        }
3273
                    }
3274
                } else {
3275
                    if ($pluginType & Core::COMPILABLE_PLUGIN) {
3276
                        if ($mapped) {
3277
                            throw new CompilationException($this, 'The @ operator can not be used on compiled plugins.');
3278
                        }
3279
                        if ($pluginType & Core::CUSTOM_PLUGIN) {
3280
                            $callback = $this->customPlugins[$func]['callback'];
3281
                            if (!is_array($callback)) {
3282
                                if (!method_exists($callback, 'compile')) {
3283
                                    throw new Exception('Custom plugin ' . $func . ' must implement the "compile" method to be compilable, or you should provide a full callback to the method to use');
3284
                                }
3285
                                if (($ref = new ReflectionMethod($callback, 'compile')) && $ref->isStatic()) {
3286
                                    $funcCompiler = array($callback, 'compile');
3287
                                } else {
3288
                                    $funcCompiler = array(new $callback(), 'compile');
3289
                                }
3290
                            } else {
3291
                                $funcCompiler = $callback;
3292
                            }
3293
                        } else {
3294
                            if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
3295
                                $funcCompiler = array('Plugin' . Core::toCamelCase($func), 'compile');
3296
                            } else {
3297
                                $funcCompiler = array(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func), 'compile');
3298
                            }
3299
                            array_unshift($params, $this);
3300
                        }
3301
                        $output = call_user_func_array($funcCompiler, $params);
3302
                    } else {
3303
                        $params = self::implode_r($params);
3304
3305
                        if ($pluginType & Core::CUSTOM_PLUGIN) {
3306
                            if (is_object($callback[0])) {
3307
                                if (is_array($this->getCore()->getCustomPlugin($func))) {
3308
                                    $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array($this->plugins[\'' . $func . '\'][\'callback\'][0], \'' . $callback[1] . '\'), array(' . $params . '))';
3309 View Code Duplication
                                } else {
3310
                                    $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array($this->getCustomPlugin(\'' . $func . '\'), \'' . $callback[1] . '\'), array(' . $params . '))';
3311
                                }
3312 View Code Duplication
                            } else {
3313
                                $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), array(' . $params . '))';
3314
                            }
3315 View Code Duplication
                        } elseif ($mapped) {
3316
                            $output = '$this->arrayMap(array($this->getObjectPlugin(\''.
3317
                                Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) . '\'), 
3318
                            \'process\'), array(' . $params . '))';
3319
                        } else {
3320
                            if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
3321
                                $output = '$this->classCall(\'Plugin' . Core::toCamelCase($func) . '\', array(' . $params . '))';
3322 View Code Duplication
                            } elseif (class_exists(Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($func)) !== false) {
3323
                                $output = '$this->classCall(\'' . Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . $func . '\', array(' . $params . '))';
3324
                            } elseif (class_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func)) !== false) {
3325
                                $output = '$this->classCall(\'' . Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . $func . '\', array(' . $params . '))';
3326
                            } else {
3327
                                $output = '$this->classCall(\'' . $func . '\', array(' . $params . '))';
3328
                            }
3329
                        }
3330
                    }
3331
                }
3332
            }
3333
        }
3334
3335
        if ($curBlock === 'namedparam') {
3336
            return array($output, $output);
3337
        } elseif ($curBlock === 'var' || $m[1] === null) {
3338
            return $output;
3339
        } elseif ($curBlock === 'string' || $curBlock === 'root') {
3340
            return $m[1] . '.' . $output . '.' . $m[1] . (isset($add) ? $add : null);
3341
        }
3342
3343
        return '';
3344
    }
3345
3346
    /**
3347
     * Recursively implodes an array in a similar manner as var_export() does but with some tweaks
3348
     * to handle pre-compiled values and the fact that we do not need to enclose everything with
3349
     * "array" and do not require top-level keys to be displayed.
3350
     *
3351
     * @param array $params        the array to implode
3352
     * @param bool  $recursiveCall if set to true, the function outputs key names for the top level
3353
     *
3354
     * @return string the imploded array
3355
     */
3356
    public static function implode_r(array $params, $recursiveCall = false)
3357
    {
3358
        $out = '';
3359
        foreach ($params as $k => $p) {
3360
            if (is_array($p)) {
3361
                $out2 = 'array(';
3362
                foreach ($p as $k2 => $v) {
3363
                    $out2 .= var_export($k2, true) . ' => ' . (is_array($v) ? 'array(' . self::implode_r($v, true) . ')' : $v) . ', ';
3364
                }
3365
                $p = rtrim($out2, ', ') . ')';
3366
            }
3367
            if ($recursiveCall) {
3368
                $out .= var_export($k, true) . ' => ' . $p . ', ';
3369
            } else {
3370
                $out .= $p . ', ';
3371
            }
3372
        }
3373
3374
        return rtrim($out, ', ');
3375
    }
3376
3377
    /**
3378
     * Returns the plugin type of a plugin and adds it to the used plugins array if required.
3379
     *
3380
     * @param string $name plugin name, as found in the template
3381
     *
3382
     * @return int type as a multi bit flag composed of the Dwoo plugin types constants
3383
     * @throws Exception
3384
     * @throws SecurityException
3385
     * @throws Exception
3386
     */
3387
    protected function getPluginType($name)
3388
    {
3389
        $pluginType = - 1;
3390
3391
        if (($this->securityPolicy === null && (function_exists($name) || strtolower($name) === 'isset' || strtolower($name) === 'empty')) || ($this->securityPolicy !== null && array_key_exists(strtolower($name), $this->securityPolicy->getAllowedPhpFunctions()) !== false)) {
3392
            $phpFunc = true;
3393
        } elseif ($this->securityPolicy !== null && function_exists($name) && array_key_exists(strtolower($name), $this->securityPolicy->getAllowedPhpFunctions()) === false) {
3394
            throw new SecurityException('Call to a disallowed php function : ' . $name);
3395
        }
3396
3397
        while ($pluginType <= 0) {
3398
            // Template plugin compilable
3399
            if (isset($this->templatePlugins[$name])) {
3400
                $pluginType = Core::TEMPLATE_PLUGIN | Core::COMPILABLE_PLUGIN;
3401
            } // Custom plugin
3402
            elseif (isset($this->customPlugins[$name])) {
3403
                $pluginType = $this->customPlugins[$name]['type'] | Core::CUSTOM_PLUGIN;
3404
            } // Class blocks plugin
3405
            elseif (class_exists(Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($name)) !== false) {
3406
                $pluginType = Core::CLASS_PLUGIN;
3407
                if (is_subclass_of(Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($name), 'Dwoo\Block\Plugin')) {
3408
                    $pluginType += Core::BLOCK_PLUGIN;
3409
                }
3410
                $interfaces = class_implements(Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($name));
3411 View Code Duplication
                if (in_array('Dwoo\ICompilable', $interfaces) !== false || in_array('Dwoo\ICompilable\Block', $interfaces) !== false) {
3412
                    $pluginType |= Core::COMPILABLE_PLUGIN;
3413
                }
3414
            } // Class functions plugin
3415
            elseif (class_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($name)) !== false) {
3416
                $pluginType = Core::FUNC_PLUGIN + Core::CLASS_PLUGIN;
3417
                $interfaces = class_implements(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($name));
3418 View Code Duplication
                if (in_array('Dwoo\ICompilable', $interfaces) !== false || in_array('Dwoo\ICompilable\Block', $interfaces) !== false) {
3419
                    $pluginType |= Core::COMPILABLE_PLUGIN;
3420
                }
3421
            } // Class without namespace
3422
            elseif (class_exists('Plugin' . Core::toCamelCase($name)) !== false) {
3423
                $pluginType = Core::CLASS_PLUGIN;
3424
                $interfaces = class_implements('Plugin' . Core::toCamelCase($name));
3425 View Code Duplication
                if (in_array('Dwoo\ICompilable', $interfaces) !== false || in_array('Dwoo\ICompilable\Block', $interfaces) !== false) {
3426
                    $pluginType |= Core::COMPILABLE_PLUGIN;
3427
                }
3428
            } // Function plugin (with/without namespaces)
3429
            elseif (function_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase ($name)) !==
3430
                false || function_exists('Plugin' . Core::toCamelCase($name)) !== false) {
3431
                $pluginType = Core::FUNC_PLUGIN;
3432
            } // Function plugin compile (with/without namespaces)
3433
            elseif (function_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($name) .
3434
                    'Compile') !== false || function_exists('Plugin' . Core::toCamelCase($name) . 'Compile') !==
3435
                false) {
3436
                $pluginType = Core::FUNC_PLUGIN | Core::COMPILABLE_PLUGIN;
3437
            } // Helper plugin class compile
3438 View Code Duplication
            elseif (class_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($name)) !== false) {
3439
                $pluginType = Core::CLASS_PLUGIN | Core::COMPILABLE_PLUGIN;
3440
            } // Helper plugin function compile
3441 View Code Duplication
            elseif (function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($name) . 'Compile') !== false) {
3442
                $pluginType = Core::FUNC_PLUGIN | Core::COMPILABLE_PLUGIN;
3443
            } // Smarty modifier
3444
            elseif (function_exists('smarty_modifier_' . $name) !== false) {
3445
                $pluginType = Core::SMARTY_MODIFIER;
3446
            } // Smarty function
3447
            elseif (function_exists('smarty_function_' . $name) !== false) {
3448
                $pluginType = Core::SMARTY_FUNCTION;
3449
            } // Smarty block
3450
            elseif (function_exists('smarty_block_' . $name) !== false) {
3451
                $pluginType = Core::SMARTY_BLOCK;
3452
            } // Everything else
3453
            else {
3454
                if ($pluginType === - 1) {
3455
                    try {
3456
                        $this->getCore()->getLoader()->loadPlugin('Plugin' . Core::toCamelCase($name));
3457
                    }
3458
                    catch (Exception $e) {
3459
                        if (isset($phpFunc)) {
3460
                            $pluginType = Core::NATIVE_PLUGIN;
3461
                        } elseif (is_object($this->getCore()->getPluginProxy()) && $this->getCore()->getPluginProxy()->handles($name)) {
3462
                            $pluginType = Core::PROXY_PLUGIN;
3463
                            break;
3464
                        } else {
3465
                            throw $e;
3466
                        }
3467
                    }
3468
                } else {
3469
                    throw new Exception('Plugin "' . $name . '" could not be found, type:' . $pluginType);
3470
                }
3471
                ++ $pluginType;
3472
            }
3473
        }
3474
3475
        if (($pluginType & Core::COMPILABLE_PLUGIN) === 0 && ($pluginType & Core::NATIVE_PLUGIN) === 0 && ($pluginType & Core::PROXY_PLUGIN) === 0) {
3476
            $this->addUsedPlugin(Core::toCamelCase($name), $pluginType);
3477
        }
3478
3479
        return $pluginType;
3480
    }
3481
3482
    /**
3483
     * Allows a plugin to load another one at compile time, this will also mark
3484
     * it as used by this template so it will be loaded at runtime (which can be
3485
     * useful for compiled plugins that rely on another plugin when their compiled
3486
     * code runs).
3487
     *
3488
     * @param string $name the plugin name
3489
     *
3490
     * @return void
3491
     */
3492
    public function loadPlugin($name)
3493
    {
3494
        $this->getPluginType($name);
3495
    }
3496
3497
    /**
3498
     * Runs htmlentities over the matched <?php ?> blocks when the security policy enforces that.
3499
     *
3500
     * @param array $match matched php block
3501
     *
3502
     * @return string the htmlentities-converted string
3503
     */
3504
    protected function phpTagEncodingHelper($match)
3505
    {
3506
        return htmlspecialchars($match[0]);
3507
    }
3508
3509
    /**
3510
     * Maps the parameters received from the template onto the parameters required by the given callback.
3511
     *
3512
     * @param array    $params   the array of parameters
3513
     * @param callback $callback the function or method to reflect on to find out the required parameters
3514
     * @param int      $callType the type of call in the template, 0 = no params, 1 = php-style call, 2 = named
3515
     *                           parameters call
3516
     * @param array    $map      the parameter map to use, if not provided it will be built from the callback
3517
     *
3518
     * @return array parameters sorted in the correct order with missing optional parameters filled
3519
     * @throws CompilationException
3520
     */
3521
    protected function mapParams(array $params, $callback, $callType = 2, $map = null)
3522
    {
3523
        if (!$map) {
3524
            $map = $this->getParamMap($callback);
3525
        }
3526
3527
        $paramlist = array();
3528
3529
        // transforms the parameter array from (x=>array('paramname'=>array(values))) to (paramname=>array(values))
3530
        $ps = array();
3531
        foreach ($params as $p) {
3532
            if (is_array($p[1])) {
3533
                $ps[$p[0]] = $p[1];
3534
            } else {
3535
                $ps[] = $p;
3536
            }
3537
        }
3538
3539
        // loops over the param map and assigns values from the template or default value for unset optional params
3540
        foreach ($map as $k => $v){
3541
            if ($v[0] === '*') {
3542
                // "rest" array parameter, fill every remaining params in it and then break
3543
                if (count($ps) === 0) {
3544
                    if ($v[1] === false) {
3545
                        throw new CompilationException(
3546
                            $this, 'Rest argument missing for ' . str_replace(
3547
                                array(
3548
                                    Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin',
3549
                                'Compile'
3550
                                ), '', (is_array($callback) ? $callback[0] : $callback)
3551
                            )
3552
                        );
3553
                    } else {
3554
                        break;
3555
                    }
3556
                }
3557
                $tmp  = array();
3558
                $tmp2 = array();
3559
                $tmp3 = array();
3560
                foreach ($ps as $i => $p) {
3561
                    $tmp[$i]  = $p[0];
3562
                    $tmp2[$i] = $p[1];
3563
                    $tmp3[$i] = isset($p[2]) ? $p[2] : 0;
3564
                    unset($ps[$i]);
3565
                }
3566
                $paramlist[$v[0]] = array($tmp, $tmp2, $tmp3);
3567
                unset($tmp, $tmp2, $i, $p);
3568
                break;
3569
            } elseif (isset($ps[$v[0]])) {
3570
                // parameter is defined as named param
3571
                $paramlist[$v[0]] = $ps[$v[0]];
3572
                unset($ps[$v[0]]);
3573
            } elseif (isset($ps[$k])) {
3574
                // parameter is defined as ordered param
3575
                $paramlist[$v[0]] = $ps[$k];
3576
                unset($ps[$k]);
3577
            } elseif ($v[1] === false) {
3578
                // parameter is not defined and not optional, throw error
3579
                if (is_array($callback)) {
3580
                    if (is_object($callback[0])) {
3581
                        $name = get_class($callback[0]) . '::' . $callback[1];
3582
                    } else {
3583
                        $name = $callback[0];
3584
                    }
3585
                } else {
3586
                    $name = $callback;
3587
                }
3588
3589
                throw new CompilationException(
3590
                    $this, 'Argument ' . $k . '/' . $v[0] . ' missing for ' . str_replace(
3591
                        array(
3592
                            Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin',
3593
                        'Compile'
3594
                        ), '', $name
3595
                    )
3596
                );
3597
            } elseif ($v[2] === null) {
3598
                // enforce lowercased null if default value is null (php outputs NULL with var export)
3599
                $paramlist[$v[0]] = array('null', null, self::T_NULL);
3600
            } else {
3601
                // outputs default value with var_export
3602
                $paramlist[$v[0]] = array(var_export($v[2], true), $v[2]);
3603
            }
3604
        }
3605
3606
        if (count($ps)) {
3607
            foreach ($ps as $i => $p) {
3608
                array_push($paramlist, $p);
3609
            }
3610
        }
3611
3612
        return $paramlist;
3613
    }
3614
3615
    /**
3616
     * Returns the parameter map of the given callback, it filters out entries typed as Dwoo and Compiler and turns the
3617
     * rest parameter into a "*".
3618
     *
3619
     * @param callback $callback the function/method to reflect on
3620
     *
3621
     * @return array processed parameter map
3622
     */
3623
    protected function getParamMap($callback)
3624
    {
3625
        if (is_null($callback)) {
3626
            return array(array('*', true));
3627
        }
3628
        if (is_array($callback)) {
3629
            $ref = new ReflectionMethod($callback[0], $callback[1]);
3630
        } else {
3631
            $ref = new ReflectionFunction($callback);
3632
        }
3633
3634
        $out = array();
3635
        foreach ($ref->getParameters() as $param) {
3636
            if (($class = $param->getClass()) !== null && $class->name === 'Dwoo\Core') {
3637
                continue;
3638
            }
3639
            if (($class = $param->getClass()) !== null && $class->name === 'Dwoo\Compiler') {
3640
                continue;
3641
            }
3642
            if ($param->getName() === 'rest' && $param->isArray() === true) {
3643
                $out[] = array('*', $param->isOptional(), null);
3644
                continue;
3645
            }
3646
            $out[] = array(
3647
                $param->getName(),
3648
                $param->isOptional(),
3649
                $param->isOptional() ? $param->getDefaultValue() : null
3650
            );
3651
        }
3652
3653
        return $out;
3654
    }
3655
3656
    /**
3657
     * Returns a default instance of this compiler, used by default by all Dwoo templates that do not have a
3658
     * specific compiler assigned and when you do not override the default compiler factory function.
3659
     *
3660
     * @see    Core::setDefaultCompilerFactory()
3661
     * @return Compiler
3662
     */
3663
    public static function compilerFactory()
3664
    {
3665
        if (self::$instance === null) {
3666
            self::$instance = new self();
3667
        }
3668
3669
        return self::$instance;
3670
    }
3671
}
3672