Completed
Push — master ( bf2930...494091 )
by David
07:08 queued 03:26
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 Modified BSD License
12
 * @version   1.3.2
13
 * @date      2017-01-04
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 $dwoo;
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->getDwoo()->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 getDwoo()
654
    {
655
        return $this->dwoo;
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      $dwoo
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 $dwoo, ITemplate $template)
713
    {
714
        // init vars
715
        //		$compiled = '';
716
        $tpl                  = $template->getSource();
717
        $ptr                  = 0;
718
        $this->dwoo           = $dwoo;
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::BLOCK_PLUGIN:
879 View Code Duplication
                case Core::CLASS_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::FUNC_PLUGIN:
889
                    if (function_exists('Plugin' . $plugin) !== false) {
890
                        $output .= "if (function_exists('" . "Plugin" . $plugin . "')===false)".
891
                        "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
892
                    } else {
893
                        $output .= "if (function_exists('" . Core::NAMESPACE_PLUGINS_FUNCTIONS . "Plugin" . $plugin . "')===false)".
894
                        "\n\t\$this->getLoader()->loadPlugin('Plugin$plugin');\n";
895
                    }
896
                    break;
897
                case Core::SMARTY_MODIFIER:
898
                    $output .= "if (function_exists('smarty_modifier_$plugin')===false)".
899
                    "\n\t\$this->getLoader()->loadPlugin('$plugin');\n";
900
                    break;
901
                case Core::SMARTY_FUNCTION:
902
                    $output .= "if (function_exists('smarty_function_$plugin')===false)".
903
                    "\n\t\$this->getLoader()->loadPlugin('$plugin');\n";
904
                    break;
905
                case Core::SMARTY_BLOCK:
906
                    $output .= "if (function_exists('smarty_block_$plugin')===false)".
907
                    "\n\t\$this->getLoader()->loadPlugin('$plugin');\n";
908
                    break;
909
                case Core::PROXY_PLUGIN:
910
                    $output .= $this->getDwoo()->getPluginProxy()->getLoader($plugin);
911
                    break;
912
                default:
913
                    throw new CompilationException($this, 'Type error for ' . $plugin . ' with type' . $type);
914
            }
915
        }
916
917
        foreach ($this->templatePlugins as $function => $attr) {
918
            if (isset($attr['called']) && $attr['called'] === true && !isset($attr['checked'])) {
919
                $this->resolveSubTemplateDependencies($function);
920
            }
921
        }
922
        foreach ($this->templatePlugins as $function) {
923
            if (isset($function['called']) && $function['called'] === true) {
924
                $output .= $function['body'] . PHP_EOL;
925
            }
926
        }
927
928
        $output .= $compiled . "\n?>";
929
930
        $output = preg_replace('/(?<!;|\}|\*\/|\n|\{)(\s*' . preg_quote(self::PHP_CLOSE, '/') . preg_quote(self::PHP_OPEN, '/') . ')/', ";\n", $output);
931
        $output = str_replace(self::PHP_CLOSE . self::PHP_OPEN, "\n", $output);
932
933
        // handle <?xml tag at the beginning
934
        $output = preg_replace('#(/\* template body \*/ \?>\s*)<\?xml#is', '$1<?php echo \'<?xml\'; ?>', $output);
935
936
        // add another line break after PHP closing tags that have a line break following,
937
        // as we do not know whether it's intended, and PHP will strip it otherwise
938
        $output = preg_replace('/(?<!"|<\?xml)\s*\?>\n/', '$0' . "\n", $output);
939
940
        if ($this->debug) {
941
            echo '=============================================================================================' . "\n";
942
            $lines = preg_split('{\r\n|\n|<br />}', $output);
943
            array_shift($lines);
944
            foreach ($lines as $i => $line) {
945
                echo ($i + 1) . '. ' . $line . "\r\n";
946
            }
947
            echo '=============================================================================================' . "\n";
948
        }
949
950
        $this->template = $this->dwoo = null;
951
        $tpl            = null;
952
953
        return $output;
954
    }
955
956
    /**
957
     * Checks what sub-templates are used in every sub-template so that we're sure they are all compiled.
958
     *
959
     * @param string $function the sub-template name
960
     */
961
    protected function resolveSubTemplateDependencies($function)
962
    {
963
        if ($this->debug) {
964
            echo 'Compiler::' . __FUNCTION__ . "\n";
965
        }
966
967
        $body = $this->templatePlugins[$function]['body'];
968
        foreach ($this->templatePlugins as $func => $attr) {
969
            if ($func !== $function && !isset($attr['called']) && strpos($body, Core::NAMESPACE_PLUGINS_FUNCTIONS .
970
            'Plugin' . Core::toCamelCase($func)) !== false) {
971
                $this->templatePlugins[$func]['called'] = true;
972
                $this->resolveSubTemplateDependencies($func);
973
            }
974
        }
975
        $this->templatePlugins[$function]['checked'] = true;
976
    }
977
978
    /**
979
     * Adds compiled content to the current block.
980
     *
981
     * @param string $content   the content to push
982
     * @param int    $lineCount newlines count in content, optional
983
     *
984
     * @throws CompilationException
985
     */
986
    public function push($content, $lineCount = null)
987
    {
988
        if ($lineCount === null) {
989
            $lineCount = substr_count($content, "\n");
990
        }
991
992
        if ($this->curBlock['buffer'] === null && count($this->stack) > 1) {
993
            // buffer is not initialized yet (the block has just been created)
994
            $this->stack[count($this->stack) - 2]['buffer'] .= (string)$content;
995
            $this->curBlock['buffer'] = '';
996
        } else {
997
            if (!isset($this->curBlock['buffer'])) {
998
                throw new CompilationException($this, 'The template has been closed too early, you probably have an extra block-closing tag somewhere');
999
            }
1000
            // append current content to current block's buffer
1001
            $this->curBlock['buffer'] .= (string)$content;
1002
        }
1003
        $this->line += $lineCount;
1004
    }
1005
1006
    /**
1007
     * Sets the scope.
1008
     * set to null if the scope becomes "unstable" (i.e. too variable or unknown) so that
1009
     * variables are compiled in a more evaluative way than just $this->scope['key']
1010
     *
1011
     * @param mixed $scope    a string i.e. "level1.level2" or an array i.e. array("level1", "level2")
1012
     * @param bool  $absolute if true, the scope is set from the top level scope and not from the current scope
1013
     *
1014
     * @return array the current scope tree
1015
     */
1016
    public function setScope($scope, $absolute = false)
1017
    {
1018
        $old = $this->scopeTree;
1019
1020
        if ($scope === null) {
1021
            unset($this->scope);
1022
            $this->scope = null;
1023
        }
1024
1025
        if (is_array($scope) === false) {
1026
            $scope = explode('.', $scope);
1027
        }
1028
1029
        if ($absolute === true) {
1030
            $this->scope     = &$this->data;
1031
            $this->scopeTree = array();
1032
        }
1033
1034
        while (($bit = array_shift($scope)) !== null) {
1035
            if ($bit === '_parent' || $bit === '_') {
1036
                array_pop($this->scopeTree);
1037
                reset($this->scopeTree);
1038
                $this->scope = &$this->data;
1039
                $cnt         = count($this->scopeTree);
1040 View Code Duplication
                for ($i = 0; $i < $cnt; ++ $i) {
1041
                    $this->scope = &$this->scope[$this->scopeTree[$i]];
1042
                }
1043 View Code Duplication
            } elseif ($bit === '_root' || $bit === '__') {
1044
                $this->scope     = &$this->data;
1045
                $this->scopeTree = array();
1046
            } elseif (isset($this->scope[$bit])) {
1047
                $this->scope       = &$this->scope[$bit];
1048
                $this->scopeTree[] = $bit;
1049
            } else {
1050
                $this->scope[$bit] = array();
1051
                $this->scope       = &$this->scope[$bit];
1052
                $this->scopeTree[] = $bit;
1053
            }
1054
        }
1055
1056
        return $old;
1057
    }
1058
1059
    /**
1060
     * Adds a block to the top of the block stack.
1061
     *
1062
     * @param string $type      block type (name)
1063
     * @param array  $params    the parameters array
1064
     * @param int    $paramtype the parameters type (see mapParams), 0, 1 or 2
1065
     *
1066
     * @return string the preProcessing() method's output
1067
     */
1068
    public function addBlock($type, array $params, $paramtype)
1069
    {
1070
        if ($this->debug) {
1071
            echo 'Compiler::' . __FUNCTION__ . "\n";
1072
        }
1073
1074
        $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($type);
1075
        if (class_exists($class) === false) {
1076
            $this->getDwoo()->getLoader()->loadPlugin($type);
1077
        }
1078
        $params = $this->mapParams($params, array($class, 'init'), $paramtype);
1079
1080
        $this->stack[]  = array(
1081
            'type'   => $type,
1082
            'params' => $params,
1083
            'custom' => false,
1084
            'class'  => $class,
1085
            'buffer' => null
1086
        );
1087
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1088
1089
        return call_user_func(array($class, 'preProcessing'), $this, $params, '', '', $type);
1090
    }
1091
1092
    /**
1093
     * Adds a custom block to the top of the block stack.
1094
     *
1095
     * @param string $type      block type (name)
1096
     * @param array  $params    the parameters array
1097
     * @param int    $paramtype the parameters type (see mapParams), 0, 1 or 2
1098
     *
1099
     * @return string the preProcessing() method's output
1100
     */
1101
    public function addCustomBlock($type, array $params, $paramtype)
1102
    {
1103
        $callback = $this->customPlugins[$type]['callback'];
1104
        if (is_array($callback)) {
1105
            $class = is_object($callback[0]) ? get_class($callback[0]) : $callback[0];
1106
        } else {
1107
            $class = $callback;
1108
        }
1109
1110
        $params = $this->mapParams($params, array($class, 'init'), $paramtype);
1111
1112
        $this->stack[]  = array(
1113
            'type'   => $type,
1114
            'params' => $params,
1115
            'custom' => true,
1116
            'class'  => $class,
1117
            'buffer' => null
1118
        );
1119
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1120
1121
        return call_user_func(array($class, 'preProcessing'), $this, $params, '', '', $type);
1122
    }
1123
1124
    /**
1125
     * Injects a block at the top of the plugin stack without calling its preProcessing method.
1126
     * used by {else} blocks to re-add themselves after having closed everything up to their parent
1127
     *
1128
     * @param string $type   block type (name)
1129
     * @param array  $params parameters array
1130
     */
1131
    public function injectBlock($type, array $params)
1132
    {
1133
        if ($this->debug) {
1134
            echo 'Compiler::' . __FUNCTION__ . "\n";
1135
        }
1136
1137
        $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($type);
1138
        if (class_exists($class) === false) {
1139
            $this->getDwoo()->getLoader()->loadPlugin($type);
1140
        }
1141
        $this->stack[]  = array(
1142
            'type'   => $type,
1143
            'params' => $params,
1144
            'custom' => false,
1145
            'class'  => $class,
1146
            'buffer' => null
1147
        );
1148
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1149
    }
1150
1151
    /**
1152
     * Removes the closest-to-top block of the given type and all other
1153
     * blocks encountered while going down the block stack.
1154
     *
1155
     * @param string $type block type (name)
1156
     *
1157
     * @return string the output of all postProcessing() method's return values of the closed blocks
1158
     * @throws CompilationException
1159
     */
1160
    public function removeBlock($type)
1161
    {
1162
        if ($this->debug) {
1163
            echo 'Compiler::' . __FUNCTION__ . "\n";
1164
        }
1165
1166
        $output = '';
1167
1168
        $pluginType = $this->getPluginType($type);
1169
        if ($pluginType & Core::SMARTY_BLOCK) {
1170
            $type = 'Smartyinterface';
1171
        }
1172
        while (true) {
1173
            while ($top = array_pop($this->stack)) {
1174 View Code Duplication
                if ($top['custom']) {
1175
                    $class = $top['class'];
1176
                } else {
1177
                    $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($top['type']);
1178
                }
1179
                if (count($this->stack)) {
1180
                    $this->curBlock = &$this->stack[count($this->stack) - 1];
1181
                    $this->push(call_user_func(array(
1182
                        $class,
1183
                        'postProcessing'
1184
                    ), $this, $top['params'], '', '', $top['buffer']), 0);
1185
                } else {
1186
                    $null           = null;
1187
                    $this->curBlock = &$null;
1188
                    $output         = call_user_func(
1189
                        array(
1190
                        $class,
1191
                        'postProcessing'
1192
                        ), $this, $top['params'], '', '', $top['buffer']
1193
                    );
1194
                }
1195
1196
                if ($top['type'] === $type) {
1197
                    break 2;
1198
                }
1199
            }
1200
1201
            throw new CompilationException($this, 'Syntax malformation, a block of type "' . $type . '" was closed but was not opened');
1202
            break;
1203
        }
1204
1205
        return $output;
1206
    }
1207
1208
    /**
1209
     * Returns a reference to the first block of the given type encountered and
1210
     * optionally closes all blocks until it finds it
1211
     * this is mainly used by {else} plugins to close everything that was opened
1212
     * between their parent and themselves.
1213
     *
1214
     * @param string $type       the block type (name)
1215
     * @param bool   $closeAlong whether to close all blocks encountered while going down the block stack or not
1216
     *
1217
     * @return mixed &array the array is as such: array('type'=>pluginName, 'params'=>parameter array,
1218
     *               'custom'=>bool defining whether it's a custom plugin or not, for internal use)
1219
     * @throws CompilationException
1220
     */
1221
    public function &findBlock($type, $closeAlong = false)
1222
    {
1223
        if ($closeAlong === true) {
1224 View Code Duplication
            while ($b = end($this->stack)) {
1225
                if ($b['type'] === $type) {
1226
                    return $this->stack[key($this->stack)];
1227
                }
1228
                $this->push($this->removeTopBlock(), 0);
1229
            }
1230
        } else {
1231
            end($this->stack);
1232 View Code Duplication
            while ($b = current($this->stack)) {
1233
                if ($b['type'] === $type) {
1234
                    return $this->stack[key($this->stack)];
1235
                }
1236
                prev($this->stack);
1237
            }
1238
        }
1239
1240
        throw new CompilationException($this, 'A parent block of type "' . $type . '" is required and can not be found');
1241
    }
1242
1243
    /**
1244
     * Returns a reference to the current block array.
1245
     *
1246
     * @return array the array is as such: array('type'=>pluginName, 'params'=>parameter array,
1247
     *                'custom'=>bool defining whether it's a custom plugin or not, for internal use)
1248
     */
1249
    public function &getCurrentBlock()
1250
    {
1251
        return $this->curBlock;
1252
    }
1253
1254
    /**
1255
     * Removes the block at the top of the stack and calls its postProcessing() method.
1256
     *
1257
     * @return string the postProcessing() method's output
1258
     * @throws CompilationException
1259
     */
1260
    public function removeTopBlock()
1261
    {
1262
        if ($this->debug) {
1263
            echo 'Compiler::' . __FUNCTION__ . "\n";
1264
        }
1265
1266
        $o = array_pop($this->stack);
1267
        if ($o === null) {
1268
            throw new CompilationException($this, 'Syntax malformation, a block of unknown type was closed but was not opened.');
1269
        }
1270 View Code Duplication
        if ($o['custom']) {
1271
            $class = $o['class'];
1272
        } else {
1273
            $class = Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($o['type']);
1274
        }
1275
1276
        $this->curBlock = &$this->stack[count($this->stack) - 1];
1277
1278
        return call_user_func(array($class, 'postProcessing'), $this, $o['params'], '', '', $o['buffer']);
1279
    }
1280
1281
    /**
1282
     * Returns the compiled parameters (for example a variable's compiled parameter will be "$this->scope['key']") out
1283
     * of the given parameter array.
1284
     *
1285
     * @param array $params parameter array
1286
     *
1287
     * @return array filtered parameters
1288
     */
1289 View Code Duplication
    public function getCompiledParams(array $params)
1290
    {
1291
        foreach ($params as $k => $p) {
1292
            if (is_array($p)) {
1293
                $params[$k] = $p[0];
1294
            }
1295
        }
1296
1297
        return $params;
1298
    }
1299
1300
    /**
1301
     * Returns the real parameters (for example a variable's real parameter will be its key, etc) out of the given
1302
     * parameter array.
1303
     *
1304
     * @param array $params parameter array
1305
     *
1306
     * @return array filtered parameters
1307
     */
1308 View Code Duplication
    public function getRealParams(array $params)
1309
    {
1310
        foreach ($params as $k => $p) {
1311
            if (is_array($p)) {
1312
                $params[$k] = $p[1];
1313
            }
1314
        }
1315
1316
        return $params;
1317
    }
1318
1319
    /**
1320
     * Returns the token of each parameter out of the given parameter array.
1321
     *
1322
     * @param array $params parameter array
1323
     *
1324
     * @return array tokens
1325
     */
1326 View Code Duplication
    public function getParamTokens(array $params)
1327
    {
1328
        foreach ($params as $k => $p) {
1329
            if (is_array($p)) {
1330
                $params[$k] = isset($p[2]) ? $p[2] : 0;
1331
            }
1332
        }
1333
1334
        return $params;
1335
    }
1336
1337
    /**
1338
     * Entry point of the parser, it redirects calls to other parse* functions.
1339
     *
1340
     * @param string $in            the string within which we must parse something
1341
     * @param int    $from          the starting offset of the parsed area
1342
     * @param int    $to            the ending offset of the parsed area
1343
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
1344
     *                              default
1345
     * @param string $curBlock      the current parser-block being processed
1346
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
1347
     *                              or null by default
1348
     *
1349
     * @return string parsed values
1350
     * @throws CompilationException
1351
     */
1352
    protected function parse($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
1353
    {
1354
        if ($this->debug) {
1355
            echo 'Compiler::' . __FUNCTION__ . "\n";
1356
        }
1357
1358
        if ($to === null) {
1359
            $to = strlen($in);
1360
        }
1361
        $first = substr($in, $from, 1);
1362
1363
        if ($first === false) {
1364
            throw new CompilationException($this, 'Unexpected EOF, a template tag was not closed');
1365
        }
1366
1367
        while ($first === ' ' || $first === "\n" || $first === "\t" || $first === "\r") {
1368 View Code Duplication
            if ($curBlock === 'root' && substr($in, $from, strlen($this->rd)) === $this->rd) {
1369
                // end template tag
1370
                $pointer += strlen($this->rd);
1371
                if ($this->debug) {
1372
                    echo 'TEMPLATE PARSING ENDED' . "\n";
1373
                }
1374
1375
                return false;
1376
            }
1377
            ++ $from;
1378
            if ($pointer !== null) {
1379
                ++ $pointer;
1380
            }
1381
            if ($from >= $to) {
1382
                if (is_array($parsingParams)) {
1383
                    return $parsingParams;
1384
                } else {
1385
                    return '';
1386
                }
1387
            }
1388
            $first = $in[$from];
1389
        }
1390
1391
        $substr = substr($in, $from, $to - $from);
1392
1393
        if ($this->debug) {
1394
            echo 'PARSE CALL : PARSING "' . htmlentities(substr($in, $from, min($to - $from, 50))) . (($to - $from) > 50 ? '...' : '') . '" @ ' . $from . ':' . $to . ' in ' . $curBlock . ' : pointer=' . $pointer . "\n";
1395
        }
1396
        $parsed = '';
1397
1398
        if ($curBlock === 'root' && $first === '*') {
1399
            $src      = $this->getTemplateSource();
1400
            $startpos = $this->getPointer() - strlen($this->ld);
1401
            if (substr($src, $startpos, strlen($this->ld)) === $this->ld) {
1402
                if ($startpos > 0) {
1403
                    do {
1404
                        $char = substr($src, -- $startpos, 1);
1405
                        if ($char == "\n") {
1406
                            ++ $startpos;
1407
                            $whitespaceStart = true;
1408
                            break;
1409
                        }
1410
                    }
1411
                    while ($startpos > 0 && ($char == ' ' || $char == "\t"));
1412
                }
1413
1414
                if (!isset($whitespaceStart)) {
1415
                    $startpos = $this->getPointer();
1416
                } else {
1417
                    $pointer -= $this->getPointer() - $startpos;
1418
                }
1419
1420
                if ($this->allowNestedComments && strpos($src, $this->ld . '*', $this->getPointer()) !== false) {
1421
                    $comOpen  = $this->ld . '*';
1422
                    $comClose = '*' . $this->rd;
1423
                    $level    = 1;
1424
                    $ptr      = $this->getPointer();
1425
1426
                    while ($level > 0 && $ptr < strlen($src)) {
1427
                        $open  = strpos($src, $comOpen, $ptr);
1428
                        $close = strpos($src, $comClose, $ptr);
1429
1430
                        if ($open !== false && $close !== false) {
1431
                            if ($open < $close) {
1432
                                $ptr = $open + strlen($comOpen);
1433
                                ++ $level;
1434
                            } else {
1435
                                $ptr = $close + strlen($comClose);
1436
                                -- $level;
1437
                            }
1438
                        } elseif ($open !== false) {
1439
                            $ptr = $open + strlen($comOpen);
1440
                            ++ $level;
1441
                        } elseif ($close !== false) {
1442
                            $ptr = $close + strlen($comClose);
1443
                            -- $level;
1444
                        } else {
1445
                            $ptr = strlen($src);
1446
                        }
1447
                    }
1448
                    $endpos = $ptr - strlen('*' . $this->rd);
1449 View Code Duplication
                } else {
1450
                    $endpos = strpos($src, '*' . $this->rd, $startpos);
1451
                    if ($endpos == false) {
1452
                        throw new CompilationException($this, 'Un-ended comment');
1453
                    }
1454
                }
1455
                $pointer += $endpos - $startpos + strlen('*' . $this->rd);
1456
                if (isset($whitespaceStart) && preg_match('#^[\t ]*\r?\n#', substr($src, $endpos + strlen('*' . $this->rd)), $m)) {
1457
                    $pointer += strlen($m[0]);
1458
                    $this->curBlock['buffer'] = substr($this->curBlock['buffer'], 0, strlen($this->curBlock['buffer']) - ($this->getPointer() - $startpos - strlen($this->ld)));
1459
                }
1460
1461
                return false;
1462
            }
1463
        }
1464
1465
        if ($first === '$') {
1466
            // var
1467
            $out    = $this->parseVar($in, $from, $to, $parsingParams, $curBlock, $pointer);
1468
            $parsed = 'var';
1469
        } elseif ($first === '%' && preg_match('#^%[a-z_\\\\]#i', $substr)) {
1470
            // Short constant
1471
            $out = $this->parseConst($in, $from, $to, $parsingParams, $curBlock, $pointer);
1472
        } elseif (($first === '"' || $first === "'") && !(is_array($parsingParams) && preg_match('#^([\'"])[a-z0-9_]+\1\s*=>?(?:\s+|[^=])#i', $substr))) {
1473
            // string
1474
            $out = $this->parseString($in, $from, $to, $parsingParams, $curBlock, $pointer);
1475
        } 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)) {
1476
            // func
1477
            $out    = $this->parseFunction($in, $from, $to, $parsingParams, $curBlock, $pointer);
1478
            $parsed = 'func';
1479
        } elseif ($first === ';') {
1480
            // instruction end
1481
            if ($this->debug) {
1482
                echo 'END OF INSTRUCTION' . "\n";
1483
            }
1484
            if ($pointer !== null) {
1485
                ++ $pointer;
1486
            }
1487
1488
            return $this->parse($in, $from + 1, $to, false, 'root', $pointer);
1489
        } elseif ($curBlock === 'root' && preg_match('#^/([a-z_][a-z0-9_]*)?#i', $substr, $match)) {
1490
            // close block
1491 View Code Duplication
            if (!empty($match[1]) && $match[1] == 'else') {
1492
                throw new CompilationException($this, 'Else blocks must not be closed explicitly, they are automatically closed when their parent block is closed');
1493
            }
1494 View Code Duplication
            if (!empty($match[1]) && $match[1] == 'elseif') {
1495
                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');
1496
            }
1497
            if ($pointer !== null) {
1498
                $pointer += strlen($match[0]);
1499
            }
1500
            if (empty($match[1])) {
1501
                if ($this->curBlock['type'] == 'else' || $this->curBlock['type'] == 'elseif') {
1502
                    $pointer -= strlen($match[0]);
1503
                }
1504
                if ($this->debug) {
1505
                    echo 'TOP BLOCK CLOSED' . "\n";
1506
                }
1507
1508
                return $this->removeTopBlock();
1509
            } else {
1510
                if ($this->debug) {
1511
                    echo 'BLOCK OF TYPE ' . $match[1] . ' CLOSED' . "\n";
1512
                }
1513
1514
                return $this->removeBlock($match[1]);
1515
            }
1516 View Code Duplication
        } elseif ($curBlock === 'root' && substr($substr, 0, strlen($this->rd)) === $this->rd) {
1517
            // end template tag
1518
            if ($this->debug) {
1519
                echo 'TAG PARSING ENDED' . "\n";
1520
            }
1521
            $pointer += strlen($this->rd);
1522
1523
            return false;
1524
        } elseif (is_array($parsingParams) && preg_match('#^(([\'"]?)[a-z0-9_]+\2\s*=' . ($curBlock === 'array' ? '>?' : '') . ')(?:\s+|[^=]).*#i', $substr, $match)) {
1525
            // named parameter
1526
            if ($this->debug) {
1527
                echo 'NAMED PARAM FOUND' . "\n";
1528
            }
1529
            $len = strlen($match[1]);
1530
            while (substr($in, $from + $len, 1) === ' ') {
1531
                ++ $len;
1532
            }
1533
            if ($pointer !== null) {
1534
                $pointer += $len;
1535
            }
1536
1537
            $output = array(
1538
                trim($match[1], " \t\r\n=>'\""),
1539
                $this->parse($in, $from + $len, $to, false, 'namedparam', $pointer)
1540
            );
1541
1542
            $parsingParams[] = $output;
1543
1544
            return $parsingParams;
1545
        } elseif (preg_match('#^(\\\\?[a-z_](?:\\\\?[a-z0-9_]+)*::\$[a-z0-9_]+)#i', $substr, $match)) {
1546
            // static member access
1547
            $parsed = 'var';
1548
            if (is_array($parsingParams)) {
1549
                $parsingParams[] = array($match[1], $match[1]);
1550
                $out             = $parsingParams;
1551
            } else {
1552
                $out = $match[1];
1553
            }
1554
            $pointer += strlen($match[1]);
1555
        } elseif ($substr !== '' && (is_array($parsingParams) || $curBlock === 'namedparam' || $curBlock === 'condition' || $curBlock === 'expression')) {
1556
            // unquoted string, bool or number
1557
            $out = $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
1558 View Code Duplication
        } else {
1559
            // parse error
1560
            throw new CompilationException($this, 'Parse error in "' . substr($in, $from, $to - $from) . '"');
1561
        }
1562
1563
        if (empty($out)) {
1564
            return '';
1565
        }
1566
1567
        $substr = substr($in, $pointer, $to - $pointer);
1568
1569
        // var parsed, check if any var-extension applies
1570
        if ($parsed === 'var') {
1571
            if (preg_match('#^\s*([/%+*-])\s*([a-z0-9]|\$)#i', $substr, $match)) {
1572
                if ($this->debug) {
1573
                    echo 'PARSING POST-VAR EXPRESSION ' . $substr . "\n";
1574
                }
1575
                // parse expressions
1576
                $pointer += strlen($match[0]) - 1;
1577
                if (is_array($parsingParams)) {
1578
                    if ($match[2] == '$') {
1579
                        $expr = $this->parseVar($in, $pointer, $to, array(), $curBlock, $pointer);
1580
                    } else {
1581
                        $expr = $this->parse($in, $pointer, $to, array(), 'expression', $pointer);
1582
                    }
1583
                    $out[count($out) - 1][0] .= $match[1] . $expr[0][0];
1584
                    $out[count($out) - 1][1] .= $match[1] . $expr[0][1];
1585
                } else {
1586
                    if ($match[2] == '$') {
1587
                        $expr = $this->parseVar($in, $pointer, $to, false, $curBlock, $pointer);
1588
                    } else {
1589
                        $expr = $this->parse($in, $pointer, $to, false, 'expression', $pointer);
1590
                    }
1591
                    if (is_array($out) && is_array($expr)) {
1592
                        $out[0] .= $match[1] . $expr[0];
1593
                        $out[1] .= $match[1] . $expr[1];
1594
                    } elseif (is_array($out)) {
1595
                        $out[0] .= $match[1] . $expr;
1596
                        $out[1] .= $match[1] . $expr;
1597
                    } elseif (is_array($expr)) {
1598
                        $out .= $match[1] . $expr[0];
1599
                    } else {
1600
                        $out .= $match[1] . $expr;
1601
                    }
1602
                }
1603
            } elseif ($curBlock === 'root' && preg_match('#^(\s*(?:[+/*%-.]=|=|\+\+|--)\s*)(.*)#s', $substr, $match)) {
1604
                if ($this->debug) {
1605
                    echo 'PARSING POST-VAR ASSIGNMENT ' . $substr . "\n";
1606
                }
1607
                // parse assignment
1608
                $value    = $match[2];
1609
                $operator = trim($match[1]);
1610
                if (substr($value, 0, 1) == '=') {
1611
                    throw new CompilationException($this, 'Unexpected "=" in <em>' . $substr . '</em>');
1612
                }
1613
1614
                if ($pointer !== null) {
1615
                    $pointer += strlen($match[1]);
1616
                }
1617
1618
                if ($operator !== '++' && $operator !== '--') {
1619
                    $parts = array();
1620
                    $ptr   = 0;
1621
                    $parts = $this->parse($value, 0, strlen($value), $parts, 'condition', $ptr);
1622
                    $pointer += $ptr;
1623
1624
                    // load if plugin
1625
                    try {
1626
                        $this->getPluginType('if');
1627
                    }
1628
                    catch (Exception $e) {
1629
                        throw new CompilationException($this, 'Assignments require the "if" plugin to be accessible');
1630
                    }
1631
1632
                    $parts  = $this->mapParams($parts, array(Core::NAMESPACE_PLUGINS_BLOCKS . 'PluginIf', 'init'), 1);
1633
                    $tokens = $this->getParamTokens($parts);
1634
                    $parts  = $this->getCompiledParams($parts);
1635
1636
                    $value = PluginIf::replaceKeywords($parts['*'], $tokens['*'], $this);
1637
                    $echo  = '';
1638
                } else {
1639
                    $value = array();
1640
                    $echo  = 'echo ';
1641
                }
1642
1643
                if ($this->autoEscape) {
1644
                    $out = preg_replace('#\(is_string\(\$tmp=(.+?)\) \? htmlspecialchars\(\$tmp, ENT_QUOTES, \$this->charset\) : \$tmp\)#', '$1', $out);
1645
                }
1646
                $out = self::PHP_OPEN . $echo . $out . $operator . implode(' ', $value) . self::PHP_CLOSE;
1647
            } elseif ($curBlock === 'array' && is_array($parsingParams) && preg_match('#^(\s*=>?\s*)#', $substr, $match)) {
1648
                // parse namedparam with var as name (only for array)
1649
                if ($this->debug) {
1650
                    echo 'VARIABLE NAMED PARAM (FOR ARRAY) FOUND' . "\n";
1651
                }
1652
                $len = strlen($match[1]);
1653
                $var = $out[count($out) - 1];
1654
                $pointer += $len;
1655
1656
                $output = array($var[0], $this->parse($substr, $len, null, false, 'namedparam', $pointer));
1657
1658
                $parsingParams[] = $output;
1659
1660
                return $parsingParams;
1661
            }
1662
        }
1663
1664
        if ($curBlock !== 'modifier' && ($parsed === 'func' || $parsed === 'var') && preg_match('#^(\|@?[a-z0-9_]+(:.*)?)+#i', $substr, $match)) {
1665
            // parse modifier on funcs or vars
1666
            $srcPointer = $pointer;
1667
            if (is_array($parsingParams)) {
1668
                $tmp                     = $this->replaceModifiers(
1669
                    array(
1670
                    null,
1671
                    null,
1672
                    $out[count($out) - 1][0],
1673
                    $match[0]
1674
                    ), $curBlock, $pointer
1675
                );
1676
                $out[count($out) - 1][0] = $tmp;
1677
                $out[count($out) - 1][1] .= substr($substr, $srcPointer, $srcPointer - $pointer);
1678
            } else {
1679
                $out = $this->replaceModifiers(array(null, null, $out, $match[0]), $curBlock, $pointer);
1680
            }
1681
        }
1682
1683
        // func parsed, check if any func-extension applies
1684
        if ($parsed === 'func' && preg_match('#^->[a-z0-9_]+(\s*\(.+|->[a-z_].*)?#is', $substr, $match)) {
1685
            // parse method call or property read
1686
            $ptr = 0;
1687
1688
            if (is_array($parsingParams)) {
1689
                $output = $this->parseMethodCall($out[count($out) - 1][1], $match[0], $curBlock, $ptr);
1690
1691
                $out[count($out) - 1][0] = $output;
1692
                $out[count($out) - 1][1] .= substr($match[0], 0, $ptr);
1693
            } else {
1694
                $out = $this->parseMethodCall($out, $match[0], $curBlock, $ptr);
1695
            }
1696
1697
            $pointer += $ptr;
1698
        }
1699
1700
        if ($curBlock === 'root' && substr($out, 0, strlen(self::PHP_OPEN)) !== self::PHP_OPEN) {
1701
            return self::PHP_OPEN . 'echo ' . $out . ';' . self::PHP_CLOSE;
1702
        } else {
1703
            return $out;
1704
        }
1705
    }
1706
1707
    /**
1708
     * Parses a function call.
1709
     *
1710
     * @param string $in            the string within which we must parse something
1711
     * @param int    $from          the starting offset of the parsed area
1712
     * @param int    $to            the ending offset of the parsed area
1713
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
1714
     *                              default
1715
     * @param string $curBlock      the current parser-block being processed
1716
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
1717
     *                              or null by default
1718
     *
1719
     * @return string parsed values
1720
     * @throws CompilationException
1721
     * @throws Exception
1722
     * @throws SecurityException
1723
     */
1724
    protected function parseFunction($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
1725
    {
1726
        $output = '';
1727
        $cmdstr = substr($in, $from, $to - $from);
1728
        preg_match('/^(\\\\?[a-z_](?:\\\\?[a-z0-9_]+)*(?:::[a-z_][a-z0-9_]*)?)(\s*' . $this->rdr . '|\s*;)?/i', $cmdstr, $match);
1729
1730 View Code Duplication
        if (empty($match[1])) {
1731
            throw new CompilationException($this, 'Parse error, invalid function name : ' . substr($cmdstr, 0, 15));
1732
        }
1733
1734
        $func = $match[1];
1735
1736
        if (!empty($match[2])) {
1737
            $cmdstr = $match[1];
1738
        }
1739
1740
        if ($this->debug) {
1741
            echo 'FUNC FOUND (' . $func . ')' . "\n";
1742
        }
1743
1744
        $paramsep = '';
1745
1746
        if (is_array($parsingParams) || $curBlock != 'root') {
1747
            $paramspos = strpos($cmdstr, '(');
1748
            $paramsep  = ')';
1749
        } elseif (preg_match_all('#^\s*[\\\\:a-z0-9_]+(\s*\(|\s+[^(])#i', $cmdstr, $match, PREG_OFFSET_CAPTURE)) {
1750
            $paramspos = $match[1][0][1];
1751
            $paramsep  = substr($match[1][0][0], - 1) === '(' ? ')' : '';
1752
            if ($paramsep === ')') {
1753
                $paramspos += strlen($match[1][0][0]) - 1;
1754
                if (substr($cmdstr, 0, 2) === 'if' || substr($cmdstr, 0, 6) === 'elseif') {
1755
                    $paramsep = '';
1756
                    if (strlen($match[1][0][0]) > 1) {
1757
                        -- $paramspos;
1758
                    }
1759
                }
1760
            }
1761
        } else {
1762
            $paramspos = false;
1763
        }
1764
1765
        $state = 0;
1766
1767
        if ($paramspos === false) {
1768
            $params = array();
1769
1770
            if ($curBlock !== 'root') {
1771
                return $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
1772
            }
1773
        } else {
1774
            if ($curBlock === 'condition') {
1775
                // load if plugin
1776
                $this->getPluginType('if');
1777
1778
                if (PluginIf::replaceKeywords(array($func), array(self::T_UNQUOTED_STRING), $this) !== array($func)) {
1779
                    return $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
1780
                }
1781
            }
1782
            $whitespace = strlen(substr($cmdstr, strlen($func), $paramspos - strlen($func)));
1783
            $paramstr   = substr($cmdstr, $paramspos + 1);
1784 View Code Duplication
            if (substr($paramstr, - 1, 1) === $paramsep) {
1785
                $paramstr = substr($paramstr, 0, - 1);
1786
            }
1787
1788
            if (strlen($paramstr) === 0) {
1789
                $params   = array();
1790
                $paramstr = '';
1791
            } else {
1792
                $ptr    = 0;
1793
                $params = array();
1794
                if ($func === 'empty') {
1795
                    $params = $this->parseVar($paramstr, $ptr, strlen($paramstr), $params, 'root', $ptr);
1796
                } else {
1797
                    while ($ptr < strlen($paramstr)) {
1798
                        while (true) {
1799
                            if ($ptr >= strlen($paramstr)) {
1800
                                break 2;
1801
                            }
1802
1803
                            if ($func !== 'if' && $func !== 'elseif' && $paramstr[$ptr] === ')') {
1804
                                if ($this->debug) {
1805
                                    echo 'PARAM PARSING ENDED, ")" FOUND, POINTER AT ' . $ptr . "\n";
1806
                                }
1807
                                break 2;
1808
                            } elseif ($paramstr[$ptr] === ';') {
1809
                                ++ $ptr;
1810
                                if ($this->debug) {
1811
                                    echo 'PARAM PARSING ENDED, ";" FOUND, POINTER AT ' . $ptr . "\n";
1812
                                }
1813
                                break 2;
1814
                            } elseif ($func !== 'if' && $func !== 'elseif' && $paramstr[$ptr] === '/') {
1815
                                if ($this->debug) {
1816
                                    echo 'PARAM PARSING ENDED, "/" FOUND, POINTER AT ' . $ptr . "\n";
1817
                                }
1818
                                break 2;
1819
                            } elseif (substr($paramstr, $ptr, strlen($this->rd)) === $this->rd) {
1820
                                if ($this->debug) {
1821
                                    echo 'PARAM PARSING ENDED, RIGHT DELIMITER FOUND, POINTER AT ' . $ptr . "\n";
1822
                                }
1823
                                break 2;
1824
                            }
1825
1826
                            if ($paramstr[$ptr] === ' ' || $paramstr[$ptr] === ',' || $paramstr[$ptr] === "\r" || $paramstr[$ptr] === "\n" || $paramstr[$ptr] === "\t") {
1827
                                ++ $ptr;
1828
                            } else {
1829
                                break;
1830
                            }
1831
                        }
1832
1833
                        if ($this->debug) {
1834
                            echo 'FUNC START PARAM PARSING WITH POINTER AT ' . $ptr . "\n";
1835
                        }
1836
1837
                        if ($func === 'if' || $func === 'elseif' || $func === 'tif') {
1838
                            $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'condition', $ptr);
1839
                        } elseif ($func === 'array') {
1840
                            $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'array', $ptr);
1841
                        } else {
1842
                            $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'function', $ptr);
1843
                        }
1844
1845 View Code Duplication
                        if ($this->debug) {
1846
                            echo 'PARAM PARSED, POINTER AT ' . $ptr . ' (' . substr($paramstr, $ptr - 1, 3) . ')' . "\n";
1847
                        }
1848
                    }
1849
                }
1850
                $paramstr = substr($paramstr, 0, $ptr);
1851
                $state    = 0;
1852
                foreach ($params as $k => $p) {
1853
                    if (is_array($p) && is_array($p[1])) {
1854
                        $state |= 2;
1855
                    } else {
1856
                        if (($state & 2) && preg_match('#^(["\'])(.+?)\1$#', $p[0], $m) && $func !== 'array') {
1857
                            $params[$k] = array($m[2], array('true', 'true'));
1858
                        } else {
1859
                            if ($state & 2 && $func !== 'array') {
1860
                                throw new CompilationException($this, 'You can not use an unnamed parameter after a named one');
1861
                            }
1862
                            $state |= 1;
1863
                        }
1864
                    }
1865
                }
1866
            }
1867
        }
1868
1869
        if ($pointer !== null) {
1870
            $pointer += (isset($paramstr) ? strlen($paramstr) : 0) + (')' === $paramsep ? 2 : ($paramspos === false ? 0 : 1)) + strlen($func) + (isset($whitespace) ? $whitespace : 0);
1871
            if ($this->debug) {
1872
                echo 'FUNC ADDS ' . ((isset($paramstr) ? strlen($paramstr) : 0) + (')' === $paramsep ? 2 : ($paramspos === false ? 0 : 1)) + strlen($func)) . ' TO POINTER' . "\n";
1873
            }
1874
        }
1875
1876
        if ($curBlock === 'method' || $func === 'do' || strstr($func, '::') !== false) {
1877
            // handle static method calls with security policy
1878
            if (strstr($func, '::') !== false && $this->securityPolicy !== null && $this->securityPolicy->isMethodAllowed(explode('::', strtolower($func))) !== true) {
1879
                throw new SecurityException('Call to a disallowed php function : ' . $func);
1880
            }
1881
            $pluginType = Core::NATIVE_PLUGIN;
1882
        } else {
1883
            $pluginType = $this->getPluginType($func);
1884
        }
1885
1886
        // Blocks plugin
1887
        if ($pluginType & Core::BLOCK_PLUGIN) {
1888
            if ($curBlock !== 'root' || is_array($parsingParams)) {
1889
                throw new CompilationException($this, 'Block plugins can not be used as other plugin\'s arguments');
1890
            }
1891
            if ($pluginType & Core::CUSTOM_PLUGIN) {
1892
                return $this->addCustomBlock($func, $params, $state);
1893
            } else {
1894
                return $this->addBlock($func, $params, $state);
1895
            }
1896
        } elseif ($pluginType & Core::SMARTY_BLOCK) {
1897
            if ($curBlock !== 'root' || is_array($parsingParams)) {
1898
                throw new CompilationException($this, 'Block plugins can not be used as other plugin\'s arguments');
1899
            }
1900
1901
            if ($state & 2) {
1902
                array_unshift($params, array('__functype', array($pluginType, $pluginType)));
1903
                array_unshift($params, array('__funcname', array($func, $func)));
1904
            } else {
1905
                array_unshift($params, array($pluginType, $pluginType));
1906
                array_unshift($params, array($func, $func));
1907
            }
1908
1909
            return $this->addBlock('smartyinterface', $params, $state);
1910
        }
1911
1912
        // Functions plugin
1913
        if ($pluginType & Core::NATIVE_PLUGIN || $pluginType & Core::SMARTY_FUNCTION || $pluginType & Core::SMARTY_BLOCK) {
1914
            $params = $this->mapParams($params, null, $state);
1915
        } elseif ($pluginType & Core::CLASS_PLUGIN) {
1916
            if ($pluginType & Core::CUSTOM_PLUGIN) {
1917
                $params = $this->mapParams(
1918
                    $params, array(
1919
                    $this->customPlugins[$func]['class'],
1920
                    $this->customPlugins[$func]['function']
1921
                ), $state);
1922
            } else {
1923
                if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
1924
                    $params = $this->mapParams($params, array(
1925
                        'Plugin' . Core::toCamelCase($func),
1926
                        ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process'
1927
                    ), $state);
1928 View Code Duplication
                } else {
1929
                    $params = $this->mapParams($params, array(
1930
                        Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func),
1931
                        ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process'
1932
                    ), $state);
1933
                }
1934
            }
1935
        } elseif ($pluginType & Core::FUNC_PLUGIN) {
1936
            if ($pluginType & Core::CUSTOM_PLUGIN) {
1937
                $params = $this->mapParams($params, $this->customPlugins[$func]['callback'], $state);
1938
            } else {
1939
                // Custom plugin
1940
                if (function_exists('Plugin' . Core::toCamelCase($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ?
1941
                        'Compile' : '')) !== false) {
1942
                    $params = $this->mapParams($params, 'Plugin' . Core::toCamelCase($func) . (($pluginType &
1943
                            Core::COMPILABLE_PLUGIN) ? 'Compile' : ''), $state);
1944
                } // Builtin helper plugin
1945
                elseif(function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) . (
1946
                    ($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : '')) !== false) {
1947
                    $params = $this->mapParams($params, Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase
1948
                        ($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : ''), $state);
1949
                } // Builtin function plugin
1950 View Code Duplication
                else {
1951
                    $params = $this->mapParams($params, Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase
1952
                        ($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : ''), $state);
1953
                }
1954
            }
1955
        } elseif ($pluginType & Core::SMARTY_MODIFIER) {
1956
            $output = 'smarty_modifier_' . $func . '(' . implode(', ', $params) . ')';
1957
        } elseif ($pluginType & Core::PROXY_PLUGIN) {
1958
            $params = $this->mapParams($params, $this->getDwoo()->getPluginProxy()->getCallback($func), $state);
1959
        } elseif ($pluginType & Core::TEMPLATE_PLUGIN) {
1960
            // transforms the parameter array from (x=>array('paramname'=>array(values))) to (paramname=>array(values))
1961
            $map = array();
1962
            foreach ($this->templatePlugins[$func]['params'] as $param => $defValue) {
1963
                if ($param == 'rest') {
1964
                    $param = '*';
1965
                }
1966
                $hasDefault = $defValue !== null;
1967
                if ($defValue === 'null') {
1968
                    $defValue = null;
1969
                } elseif ($defValue === 'false') {
1970
                    $defValue = false;
1971
                } elseif ($defValue === 'true') {
1972
                    $defValue = true;
1973
                } elseif (preg_match('#^([\'"]).*?\1$#', $defValue)) {
1974
                    $defValue = substr($defValue, 1, - 1);
1975
                }
1976
                $map[] = array($param, $hasDefault, $defValue);
1977
            }
1978
1979
            $params = $this->mapParams($params, null, $state, $map);
1980
        }
1981
1982
        // only keep php-syntax-safe values for non-block plugins
1983
        $tokens = array();
1984
        foreach ($params as $k => $p) {
1985
            $tokens[$k] = isset($p[2]) ? $p[2] : 0;
1986
            $params[$k] = $p[0];
1987
        }
1988
        if ($pluginType & Core::NATIVE_PLUGIN) {
1989
            if ($func === 'do') {
1990
                if (isset($params['*'])) {
1991
                    $output = implode(';', $params['*']) . ';';
1992
                } else {
1993
                    $output = '';
1994
                }
1995
1996
                if (is_array($parsingParams) || $curBlock !== 'root') {
1997
                    throw new CompilationException($this, 'Do can not be used inside another function or block');
1998
                } else {
1999
                    return self::PHP_OPEN . $output . self::PHP_CLOSE;
2000
                }
2001
            } else {
2002
                if (isset($params['*'])) {
2003
                    $output = $func . '(' . implode(', ', $params['*']) . ')';
2004
                } else {
2005
                    $output = $func . '()';
2006
                }
2007
            }
2008
        } elseif ($pluginType & Core::FUNC_PLUGIN) {
2009
            if ($pluginType & Core::COMPILABLE_PLUGIN) {
2010
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2011
                    $funcCompiler = $this->customPlugins[$func]['callback'];
2012
                } else {
2013
                    // Custom plugin
2014
                    if (function_exists('Plugin' . Core::toCamelCase($func) . 'Compile') !== false) {
2015
                        $funcCompiler = 'Plugin' . Core::toCamelCase($func) . 'Compile';
2016
                    } // Builtin helper plugin
2017 View Code Duplication
                    elseif(function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) .
2018
                            'Compile') !== false) {
2019
                        $funcCompiler = Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) .
2020
                            'Compile';
2021
                    } // Builtin function plugin
2022
                    else {
2023
                        $funcCompiler = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) .
2024
                            'Compile';
2025
                    }
2026
                }
2027
                array_unshift($params, $this);
2028
                if ($func === 'tif') {
2029
                    $params[] = $tokens;
2030
                }
2031
                $output = call_user_func_array($funcCompiler, $params);
2032
            } else {
2033
                array_unshift($params, '$this');
2034
                $params = self::implode_r($params);
2035
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2036
                    $callback = $this->customPlugins[$func]['callback'];
2037
                    if ($callback instanceof Closure) {
2038
                        $output = 'call_user_func($this->getCustomPlugin(\'' . $func . '\'), ' . $params . ')';
2039
                    } else {
2040
                        $output = 'call_user_func(\'' . $callback . '\', ' . $params . ')';
2041
                    }
2042
                } else {
2043
                    // Custom plugin
2044
                    if (function_exists('Plugin' . Core::toCamelCase($func)) !== false) {
2045
                        $output = 'Plugin' . Core::toCamelCase($func) . '(' . $params .
2046
                            ')';
2047
                    } // Builtin helper plugin
2048 View Code Duplication
                    elseif(function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func)) !==
2049
                        false) {
2050
                        $output = Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($func) . '(' .
2051
                            $params . ')';
2052
                    } // Builtin function plugin
2053 View Code Duplication
                    else {
2054
                        $output = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) . '(' .
2055
                            $params . ')';
2056
                    }
2057
                }
2058
            }
2059
        } elseif ($pluginType & Core::CLASS_PLUGIN) {
2060
            if ($pluginType & Core::COMPILABLE_PLUGIN) {
2061 View Code Duplication
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2062
                    $callback = $this->customPlugins[$func]['callback'];
2063
                    if (!is_array($callback)) {
2064
                        if (!method_exists($callback, 'compile')) {
2065
                            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');
2066
                        }
2067
                        if (($ref = new ReflectionMethod($callback, 'compile')) && $ref->isStatic()) {
2068
                            $funcCompiler = array($callback, 'compile');
2069
                        } else {
2070
                            $funcCompiler = array(new $callback(), 'compile');
2071
                        }
2072
                    } else {
2073
                        $funcCompiler = $callback;
2074
                    }
2075
                } else {
2076
                    if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
2077
                        $funcCompiler = array('Plugin' . Core::toCamelCase($func), 'compile');
2078
                    } else {
2079
                        $funcCompiler = array(
2080
                            Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func),
2081
                            'compile'
2082
                        );
2083
                    }
2084
                    array_unshift($params, $this);
2085
                }
2086
                $output = call_user_func_array($funcCompiler, $params);
2087
            } else {
2088
                $params = self::implode_r($params);
2089
                if ($pluginType & Core::CUSTOM_PLUGIN) {
2090
                    $callback = $this->customPlugins[$func]['callback'];
2091
                    if (!is_array($callback)) {
2092
                        if (!method_exists($callback, 'process')) {
2093
                            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');
2094
                        }
2095
                        if (($ref = new ReflectionMethod($callback, 'process')) && $ref->isStatic()) {
2096
                            $output = 'call_user_func(array(\'' . $callback . '\', \'process\'), ' . $params . ')';
2097
                        } else {
2098
                            $output = 'call_user_func(array($this->getObjectPlugin(\'' . $callback . '\'), \'process\'), ' . $params . ')';
2099
                        }
2100 View Code Duplication
                    } elseif (is_object($callback[0])) {
2101
                        $output = 'call_user_func(array($this->plugins[\'' . $func . '\'][\'callback\'][0], \'' . $callback[1] . '\'), ' . $params . ')';
2102
                    } elseif (($ref = new ReflectionMethod($callback[0], $callback[1])) && $ref->isStatic()) {
2103
                        $output = 'call_user_func(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), ' . $params . ')';
2104 View Code Duplication
                    } else {
2105
                        $output = 'call_user_func(array($this->getObjectPlugin(\'' . $callback[0] . '\'), \'' . $callback[1] . '\'), ' . $params . ')';
2106
                    }
2107
                    if (empty($params)) {
2108
                        $output = substr($output, 0, - 3) . ')';
2109
                    }
2110
                } else {
2111
                    if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
2112
                        $output = '$this->classCall(\'Plugin' . $func . '\', array(' . $params . '))';
2113 View Code Duplication
                    } elseif (class_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func)) !==
2114
                    false) {
2115
                        $output = '$this->classCall(\'' . Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . $func . '\', 
2116
                        array(' . $params . '))';
2117
                    } else{
2118
                        $output = '$this->classCall(\'' . $func . '\', array(' . $params . '))';
2119
                    }
2120
                }
2121
            }
2122
        } elseif ($pluginType & Core::PROXY_PLUGIN) {
2123
            $output = call_user_func(array($this->getDwoo()->getPluginProxy(), 'getCode'), $func, $params);
2124
        } elseif ($pluginType & Core::SMARTY_FUNCTION) {
2125
            if (isset($params['*'])) {
2126
                $params = self::implode_r($params['*'], true);
2127
            } else {
2128
                $params = '';
2129
            }
2130
2131
            if ($pluginType & Core::CUSTOM_PLUGIN) {
2132
                $callback = $this->customPlugins[$func]['callback'];
2133
                if (is_array($callback)) {
2134
                    if (is_object($callback[0])) {
2135
                        $output = 'call_user_func_array(array($this->plugins[\'' . $func . '\'][\'callback\'][0], \'' . $callback[1] . '\'), array(array(' . $params . '), $this))';
2136 View Code Duplication
                    } else {
2137
                        $output = 'call_user_func_array(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), array(array(' . $params . '), $this))';
2138
                    }
2139
                } else {
2140
                    $output = $callback . '(array(' . $params . '), $this)';
2141
                }
2142
            } else {
2143
                $output = 'smarty_function_' . $func . '(array(' . $params . '), $this)';
2144
            }
2145
        } elseif ($pluginType & Core::TEMPLATE_PLUGIN) {
2146
            array_unshift($params, '$this');
2147
            $params                                 = self::implode_r($params);
2148
            $output                                 = 'Plugin' . Core::toCamelCase($func) .
2149
                $this->templatePlugins[$func]['uuid'] . '(' . $params . ')';
2150
            $this->templatePlugins[$func]['called'] = true;
2151
        }
2152
2153 View Code Duplication
        if (is_array($parsingParams)) {
2154
            $parsingParams[] = array($output, $output);
2155
2156
            return $parsingParams;
2157
        } elseif ($curBlock === 'namedparam') {
2158
            return array($output, $output);
2159
        } else {
2160
            return $output;
2161
        }
2162
    }
2163
2164
    /**
2165
     * Parses a string.
2166
     *
2167
     * @param string $in            the string within which we must parse something
2168
     * @param int    $from          the starting offset of the parsed area
2169
     * @param int    $to            the ending offset of the parsed area
2170
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2171
     *                              default
2172
     * @param string $curBlock      the current parser-block being processed
2173
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2174
     *                              or null by default
2175
     *
2176
     * @return string parsed values
2177
     * @throws CompilationException
2178
     */
2179
    protected function parseString($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2180
    {
2181
        $substr = substr($in, $from, $to - $from);
2182
        $first  = $substr[0];
2183
2184
        if ($this->debug) {
2185
            echo 'STRING FOUND (in ' . htmlentities(substr($in, $from, min($to - $from, 50))) . (($to - $from) > 50 ? '...' : '') . ')' . "\n";
2186
        }
2187
        $strend = false;
2188
        $o      = $from + 1;
2189
        while ($strend === false) {
2190
            $strend = strpos($in, $first, $o);
2191 View Code Duplication
            if ($strend === false) {
2192
                throw new CompilationException($this, 'Unfinished string, started with ' . substr($in, $from, $to - $from));
2193
            }
2194
            if (substr($in, $strend - 1, 1) === '\\') {
2195
                $o      = $strend + 1;
2196
                $strend = false;
2197
            }
2198
        }
2199
        if ($this->debug) {
2200
            echo 'STRING DELIMITED: ' . substr($in, $from, $strend + 1 - $from) . "\n";
2201
        }
2202
2203
        $srcOutput = substr($in, $from, $strend + 1 - $from);
2204
2205
        if ($pointer !== null) {
2206
            $pointer += strlen($srcOutput);
2207
        }
2208
2209
        $output = $this->replaceStringVars($srcOutput, $first);
2210
2211
        // handle modifiers
2212
        if ($curBlock !== 'modifier' && preg_match('#^((?:\|(?:@?[a-z0-9_]+(?::.*)*))+)#i', substr($substr, $strend + 1 - $from), $match)) {
2213
            $modstr = $match[1];
2214
2215
            if ($curBlock === 'root' && substr($modstr, - 1) === '}') {
2216
                $modstr = substr($modstr, 0, - 1);
2217
            }
2218
            $modstr = str_replace('\\' . $first, $first, $modstr);
2219
            $ptr    = 0;
2220
            $output = $this->replaceModifiers(array(null, null, $output, $modstr), 'string', $ptr);
2221
2222
            $strend += $ptr;
2223
            if ($pointer !== null) {
2224
                $pointer += $ptr;
2225
            }
2226
            $srcOutput .= substr($substr, $strend + 1 - $from, $ptr);
2227
        }
2228
2229
        if (is_array($parsingParams)) {
2230
            $parsingParams[] = array($output, substr($srcOutput, 1, - 1));
2231
2232
            return $parsingParams;
2233
        } elseif ($curBlock === 'namedparam') {
2234
            return array($output, substr($srcOutput, 1, - 1));
2235
        } else {
2236
            return $output;
2237
        }
2238
    }
2239
2240
    /**
2241
     * Parses a constant.
2242
     *
2243
     * @param string $in            the string within which we must parse something
2244
     * @param int    $from          the starting offset of the parsed area
2245
     * @param int    $to            the ending offset of the parsed area
2246
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2247
     *                              default
2248
     * @param string $curBlock      the current parser-block being processed
2249
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2250
     *                              or null by default
2251
     *
2252
     * @return string parsed values
2253
     * @throws CompilationException
2254
     */
2255
    protected function parseConst($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2256
    {
2257
        $substr = substr($in, $from, $to - $from);
2258
2259
        if ($this->debug) {
2260
            echo 'CONST FOUND : ' . $substr . "\n";
2261
        }
2262
2263
        if (!preg_match('#^%([\\\\a-z0-9_:]+)#i', $substr, $m)) {
2264
            throw new CompilationException($this, 'Invalid constant');
2265
        }
2266
2267
        if ($pointer !== null) {
2268
            $pointer += strlen($m[0]);
2269
        }
2270
2271
        $output = $this->parseConstKey($m[1], $curBlock);
2272
2273 View Code Duplication
        if (is_array($parsingParams)) {
2274
            $parsingParams[] = array($output, $m[1]);
2275
2276
            return $parsingParams;
2277
        } elseif ($curBlock === 'namedparam') {
2278
            return array($output, $m[1]);
2279
        } else {
2280
            return $output;
2281
        }
2282
    }
2283
2284
    /**
2285
     * Parses a constant.
2286
     *
2287
     * @param string $key      the constant to parse
2288
     * @param string $curBlock the current parser-block being processed
2289
     *
2290
     * @return string parsed constant
2291
     */
2292
    protected function parseConstKey($key, $curBlock)
2293
    {
2294
        if ($this->securityPolicy !== null && $this->securityPolicy->getConstantHandling() === SecurityPolicy::CONST_DISALLOW) {
2295
            return 'null';
2296
        }
2297
2298 View Code Duplication
        if ($curBlock !== 'root') {
2299
            $output = '(defined("' . $key . '") ? ' . $key . ' : null)';
2300
        } else {
2301
            $output = $key;
2302
        }
2303
2304
        return $output;
2305
    }
2306
2307
    /**
2308
     * Parses a variable.
2309
     *
2310
     * @param string $in            the string within which we must parse something
2311
     * @param int    $from          the starting offset of the parsed area
2312
     * @param int    $to            the ending offset of the parsed area
2313
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2314
     *                              default
2315
     * @param string $curBlock      the current parser-block being processed
2316
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2317
     *                              or null by default
2318
     *
2319
     * @return string parsed values
2320
     * @throws CompilationException
2321
     */
2322
    protected function parseVar($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2323
    {
2324
        $substr = substr($in, $from, $to - $from);
2325
2326
        // var key
2327
        $varRegex = '(\\$?\\.?[a-z0-9\\\\_:]*(?:(?:(?:\\.|->)(?:[a-z0-9\\\\_:]+|(?R))|\\[(?:[a-z0-9\\\\_:]+|(?R)|(["\'])[^\\2]*?\\2)\\]))*)';
2328
        // method call
2329
        $methodCall = ($curBlock === 'root' || $curBlock === 'function' || $curBlock === 'namedparam' || $curBlock === 'condition' || $curBlock === 'variable' || $curBlock === 'expression' || $curBlock === 'delimited_string' ? '(\(.*)?' : '()');
2330
        // simple math expressions
2331
        $simpleMathExpressions = ($curBlock === 'root' || $curBlock === 'function' || $curBlock === 'namedparam' || $curBlock === 'condition' || $curBlock === 'variable' || $curBlock === 'delimited_string' ? '((?:(?:[+\/*%=-])(?:(?<!=)=?-?[$%][a-z0-9\\\\.[\]>_:-]+(?:\([^)]*\))?|(?<!=)=?-?[0-9\.,]*|[+-]))*)' : '()');
2332
        // modifiers
2333
        $modifiers = $curBlock !== 'modifier' ? '((?:\|(?:@?[a-z0-9\\\\_]+(?:(?::("|\').*?\5|:[^`]*))*))+)?' : '(())';
2334
2335
        $regex = '#';
2336
        $regex .= $varRegex;
2337
        $regex .= $methodCall;
2338
        $regex .= $simpleMathExpressions;
2339
        $regex .= $modifiers;
2340
        $regex .= '#i';
2341
2342
        if (preg_match($regex, $substr, $match)) {
2343
            $key = substr($match[1], 1);
2344
2345
            $matchedLength = strlen($match[0]);
2346
            $hasModifiers  = !empty($match[5]);
2347
            $hasExpression = !empty($match[4]);
2348
            $hasMethodCall = !empty($match[3]);
2349
2350
            if (substr($key, - 1) == '.') {
2351
                $key = substr($key, 0, - 1);
2352
                -- $matchedLength;
2353
            }
2354
2355
            if ($hasMethodCall) {
2356
                $matchedLength -= strlen($match[3]) + strlen(substr($match[1], strrpos($match[1], '->')));
2357
                $key        = substr($match[1], 1, strrpos($match[1], '->') - 1);
2358
                $methodCall = substr($match[1], strrpos($match[1], '->')) . $match[3];
2359
            }
2360
2361
            if ($hasModifiers) {
2362
                $matchedLength -= strlen($match[5]);
2363
            }
2364
2365
            if ($pointer !== null) {
2366
                $pointer += $matchedLength;
2367
            }
2368
2369
            // replace useless brackets by dot accessed vars and strip enclosing quotes if present
2370
            $key = preg_replace('#\[(["\']?)([^$%\[.>-]+)\1\]#', '.$2', $key);
2371
2372 View Code Duplication
            if ($this->debug) {
2373
                if ($hasMethodCall) {
2374
                    echo 'METHOD CALL FOUND : $' . $key . substr($methodCall, 0, 30) . "\n";
2375
                } else {
2376
                    echo 'VAR FOUND : $' . $key . "\n";
2377
                }
2378
            }
2379
2380
            $key = str_replace('"', '\\"', $key);
2381
2382
            $cnt = substr_count($key, '$');
2383
            if ($cnt > 0) {
2384
                $uid           = 0;
2385
                $parsed        = array($uid => '');
2386
                $current       = &$parsed;
2387
                $curTxt        = &$parsed[$uid ++];
2388
                $tree          = array();
2389
                $chars         = str_split($key, 1);
2390
                $inSplittedVar = false;
2391
                $bracketCount  = 0;
2392
2393
                while (($char = array_shift($chars)) !== null) {
2394
                    if ($char === '[') {
2395
                        if (count($tree) > 0) {
2396
                            ++ $bracketCount;
2397
                        } else {
2398
                            $tree[]        = &$current;
2399
                            $current[$uid] = array($uid + 1 => '');
2400
                            $current       = &$current[$uid ++];
2401
                            $curTxt        = &$current[$uid ++];
2402
                            continue;
2403
                        }
2404
                    } elseif ($char === ']') {
2405
                        if ($bracketCount > 0) {
2406
                            -- $bracketCount;
2407
                        } else {
2408
                            $current = &$tree[count($tree) - 1];
2409
                            array_pop($tree);
2410
                            if (current($chars) !== '[' && current($chars) !== false && current($chars) !== ']') {
2411
                                $current[$uid] = '';
2412
                                $curTxt        = &$current[$uid ++];
2413
                            }
2414
                            continue;
2415
                        }
2416
                    } elseif ($char === '$') {
2417
                        if (count($tree) == 0) {
2418
                            $curTxt        = &$current[$uid ++];
2419
                            $inSplittedVar = true;
2420
                        }
2421
                    } elseif (($char === '.' || $char === '-') && count($tree) == 0 && $inSplittedVar) {
2422
                        $curTxt        = &$current[$uid ++];
2423
                        $inSplittedVar = false;
2424
                    }
2425
2426
                    $curTxt .= $char;
2427
                }
2428
                unset($uid, $current, $curTxt, $tree, $chars);
2429
2430
                if ($this->debug) {
2431
                    echo 'RECURSIVE VAR REPLACEMENT : ' . $key . "\n";
2432
                }
2433
2434
                $key = $this->flattenVarTree($parsed);
2435
2436
                if ($this->debug) {
2437
                    echo 'RECURSIVE VAR REPLACEMENT DONE : ' . $key . "\n";
2438
                }
2439
2440
                $output = preg_replace('#(^""\.|""\.|\.""$|(\()""\.|\.""(\)))#', '$2$3', '$this->readVar("' . $key . '")');
2441
            } else {
2442
                $output = $this->parseVarKey($key, $hasModifiers ? 'modifier' : $curBlock);
2443
            }
2444
2445
2446
            // methods
2447
            if ($hasMethodCall) {
2448
                $ptr = 0;
2449
2450
                $output = $this->parseMethodCall($output, $methodCall, $curBlock, $ptr);
2451
2452
                if ($pointer !== null) {
2453
                    $pointer += $ptr;
2454
                }
2455
                $matchedLength += $ptr;
2456
            }
2457
2458
            if ($hasExpression) {
2459
                // expressions
2460
                preg_match_all('#(?:([+/*%=-])(=?-?[%$][a-z0-9\\\\.[\]>_:-]+(?:\([^)]*\))?|=?-?[0-9.,]+|\1))#i', $match[4], $expMatch);
2461
                foreach ($expMatch[1] as $k => $operator) {
2462
                    if (substr($expMatch[2][$k], 0, 1) === '=') {
2463
                        $assign = true;
2464
                        if ($operator === '=') {
2465
                            throw new CompilationException($this, 'Invalid expression <em>' . $substr . '</em>, can not use "==" in expressions');
2466
                        }
2467
                        if ($curBlock !== 'root') {
2468
                            throw new CompilationException($this, 'Invalid expression <em>' . $substr . '</em>, assignments can only be used in top level expressions like {$foo+=3} or {$foo="bar"}');
2469
                        }
2470
                        $operator .= '=';
2471
                        $expMatch[2][$k] = substr($expMatch[2][$k], 1);
2472
                    }
2473
2474
                    if (substr($expMatch[2][$k], 0, 1) === '-' && strlen($expMatch[2][$k]) > 1) {
2475
                        $operator .= '-';
2476
                        $expMatch[2][$k] = substr($expMatch[2][$k], 1);
2477
                    }
2478
                    if (($operator === '+' || $operator === '-') && $expMatch[2][$k] === $operator) {
2479
                        $output = '(' . $output . $operator . $operator . ')';
2480
                        break;
2481
                    } elseif (substr($expMatch[2][$k], 0, 1) === '$') {
2482
                        $output = '(' . $output . ' ' . $operator . ' ' . $this->parseVar($expMatch[2][$k], 0, strlen($expMatch[2][$k]), false, 'expression') . ')';
2483
                    } elseif (substr($expMatch[2][$k], 0, 1) === '%') {
2484
                        $output = '(' . $output . ' ' . $operator . ' ' . $this->parseConst($expMatch[2][$k], 0, strlen($expMatch[2][$k]), false, 'expression') . ')';
2485
                    } elseif (!empty($expMatch[2][$k])) {
2486
                        $output = '(' . $output . ' ' . $operator . ' ' . str_replace(',', '.', $expMatch[2][$k]) . ')';
2487
                    } else {
2488
                        throw new CompilationException($this, 'Unfinished expression <em>' . $substr . '</em>, missing var or number after math operator');
2489
                    }
2490
                }
2491
            }
2492
2493
            if ($this->autoEscape === true && $curBlock !== 'condition') {
2494
                $output = '(is_string($tmp=' . $output . ') ? htmlspecialchars($tmp, ENT_QUOTES, $this->charset) : $tmp)';
2495
            }
2496
2497
            // handle modifiers
2498
            if ($curBlock !== 'modifier' && $hasModifiers) {
2499
                $ptr    = 0;
2500
                $output = $this->replaceModifiers(array(null, null, $output, $match[5]), 'var', $ptr);
2501
                if ($pointer !== null) {
2502
                    $pointer += $ptr;
2503
                }
2504
                $matchedLength += $ptr;
2505
            }
2506
2507
            if (is_array($parsingParams)) {
2508
                $parsingParams[] = array($output, $key);
2509
2510
                return $parsingParams;
2511
            } elseif ($curBlock === 'namedparam') {
2512
                return array($output, $key);
2513
            } elseif ($curBlock === 'string' || $curBlock === 'delimited_string') {
2514
                return array($matchedLength, $output);
2515
            } elseif ($curBlock === 'expression' || $curBlock === 'variable') {
2516
                return $output;
2517
            } elseif (isset($assign)) {
2518
                return self::PHP_OPEN . $output . ';' . self::PHP_CLOSE;
2519
            } else {
2520
                return $output;
2521
            }
2522
        } else {
2523
            if ($curBlock === 'string' || $curBlock === 'delimited_string') {
2524
                return array(0, '');
2525
            } else {
2526
                throw new CompilationException($this, 'Invalid variable name <em>' . $substr . '</em>');
2527
            }
2528
        }
2529
    }
2530
2531
    /**
2532
     * Parses any number of chained method calls/property reads.
2533
     *
2534
     * @param string $output     the variable or whatever upon which the method are called
2535
     * @param string $methodCall method call source, starting at "->"
2536
     * @param string $curBlock   the current parser-block being processed
2537
     * @param int    $pointer    a reference to a pointer that will be increased by the amount of characters parsed
2538
     *
2539
     * @return string parsed call(s)/read(s)
2540
     */
2541
    protected function parseMethodCall($output, $methodCall, $curBlock, &$pointer)
2542
    {
2543
        $ptr = 0;
2544
        $len = strlen($methodCall);
2545
2546
        while ($ptr < $len) {
2547
            if (strpos($methodCall, '->', $ptr) === $ptr) {
2548
                $ptr += 2;
2549
            }
2550
2551
            if (in_array(
2552
                $methodCall[$ptr], array(
2553
                    ';',
2554
                    ',',
2555
                    '/',
2556
                    ' ',
2557
                    "\t",
2558
                    "\r",
2559
                    "\n",
2560
                    ')',
2561
                    '+',
2562
                    '*',
2563
                    '%',
2564
                    '=',
2565
                    '-',
2566
                    '|'
2567
                )
2568
            ) || substr($methodCall, $ptr, strlen($this->rd)) === $this->rd
2569
            ) {
2570
                // break char found
2571
                break;
2572
            }
2573
2574
            if (!preg_match('/^([a-z0-9_]+)(\(.*?\))?/i', substr($methodCall, $ptr), $methMatch)) {
2575
                break;
2576
            }
2577
2578
            if (empty($methMatch[2])) {
2579
                // property
2580
                if ($curBlock === 'root') {
2581
                    $output .= '->' . $methMatch[1];
2582
                } else {
2583
                    $output = '(($tmp = ' . $output . ') ? $tmp->' . $methMatch[1] . ' : null)';
2584
                }
2585
                $ptr += strlen($methMatch[1]);
2586
            } else {
2587
                // method
2588
                if (substr($methMatch[2], 0, 2) === '()') {
2589
                    $parsedCall = $methMatch[1] . '()';
2590
                    $ptr += strlen($methMatch[1]) + 2;
2591
                } else {
2592
                    $parsedCall = $this->parseFunction($methodCall, $ptr, strlen($methodCall), false, 'method', $ptr);
2593
                }
2594
                if ($this->securityPolicy !== null) {
2595
                    $argPos = strpos($parsedCall, '(');
2596
                    $method = strtolower(substr($parsedCall, 0, $argPos));
2597
                    $args   = substr($parsedCall, $argPos);
2598
                    if ($curBlock === 'root') {
2599
                        $output = '$this->getSecurityPolicy()->callMethod($this, ' . $output . ', ' . var_export($method, true) . ', array' . $args . ')';
2600
                    } else {
2601
                        $output = '(($tmp = ' . $output . ') ? $this->getSecurityPolicy()->callMethod($this, $tmp, ' . var_export($method, true) . ', array' . $args . ') : null)';
2602
                    }
2603 View Code Duplication
                } else {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2604
                    if ($curBlock === 'root') {
2605
                        $output .= '->' . $parsedCall;
2606
                    } else {
2607
                        $output = '(($tmp = ' . $output . ') ? $tmp->' . $parsedCall . ' : null)';
2608
                    }
2609
                }
2610
            }
2611
        }
2612
2613
        $pointer += $ptr;
2614
2615
        return $output;
2616
    }
2617
2618
    /**
2619
     * Parses a constant variable (a variable that doesn't contain another variable) and preprocesses it to save
2620
     * runtime processing time.
2621
     *
2622
     * @param string $key      the variable to parse
2623
     * @param string $curBlock the current parser-block being processed
2624
     *
2625
     * @return string parsed variable
2626
     */
2627
    protected function parseVarKey($key, $curBlock)
2628
    {
2629
        if ($key === '') {
2630
            return '$this->scope';
2631
        }
2632
        if (substr($key, 0, 1) === '.') {
2633
            $key = 'dwoo' . $key;
2634
        }
2635
        if (preg_match('#dwoo\.(get|post|server|cookies|session|env|request)((?:\.[a-z0-9_-]+)+)#i', $key, $m)) {
2636
            $global = strtoupper($m[1]);
2637
            if ($global === 'COOKIES') {
2638
                $global = 'COOKIE';
2639
            }
2640
            $key = '$_' . $global;
2641
            foreach (explode('.', ltrim($m[2], '.')) as $part) {
2642
                $key .= '[' . var_export($part, true) . ']';
2643
            }
2644 View Code Duplication
            if ($curBlock === 'root') {
2645
                $output = $key;
2646
            } else {
2647
                $output = '(isset(' . $key . ')?' . $key . ':null)';
2648
            }
2649
        } elseif (preg_match('#dwoo\\.const\\.([a-z0-9\\\\_:]+)#i', $key, $m)) {
2650
            return $this->parseConstKey($m[1], $curBlock);
2651
        } elseif ($this->scope !== null) {
2652
            if (strstr($key, '.') === false && strstr($key, '[') === false && strstr($key, '->') === false) {
2653
                if ($key === 'dwoo') {
2654
                    $output = '$this->globals';
2655
                } elseif ($key === '_root' || $key === '__') {
2656
                    $output = '$this->data';
2657
                } elseif ($key === '_parent' || $key === '_') {
2658
                    $output = '$this->readParentVar(1)';
2659
                } elseif ($key === '_key') {
2660
                    $output = '$tmp_key';
2661 View Code Duplication
                } else {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2662
                    if ($curBlock === 'root') {
2663
                        $output = '$this->scope["' . $key . '"]';
2664
                    } else {
2665
                        $output = '(isset($this->scope["' . $key . '"]) ? $this->scope["' . $key . '"] : null)';
2666
                    }
2667
                }
2668
            } else {
2669
                preg_match_all('#(\[|->|\.)?((?:[a-z0-9_]|-(?!>))+|(\\\?[\'"])[^\3]*?\3)\]?#i', $key, $m);
2670
2671
                $i = $m[2][0];
2672
                if ($i === '_parent' || $i === '_') {
2673
                    $parentCnt = 0;
2674
2675
                    while (true) {
2676
                        ++ $parentCnt;
2677
                        array_shift($m[2]);
2678
                        array_shift($m[1]);
2679
                        if (current($m[2]) === '_parent') {
2680
                            continue;
2681
                        }
2682
                        break;
2683
                    }
2684
2685
                    $output = '$this->readParentVar(' . $parentCnt . ')';
2686
                } else {
2687
                    if ($i === 'dwoo') {
2688
                        $output = '$this->globals';
2689
                        array_shift($m[2]);
2690
                        array_shift($m[1]);
2691 View Code Duplication
                    } elseif ($i === '_root' || $i === '__') {
2692
                        $output = '$this->data';
2693
                        array_shift($m[2]);
2694
                        array_shift($m[1]);
2695
                    } elseif ($i === '_key') {
2696
                        $output = '$tmp_key';
2697
                    } else {
2698
                        $output = '$this->scope';
2699
                    }
2700
2701
                    while (count($m[1]) && $m[1][0] !== '->') {
2702
                        $m[2][0] = preg_replace('/(^\\\([\'"])|\\\([\'"])$)/x', '$2$3', $m[2][0]);
2703
                        if (substr($m[2][0], 0, 1) == '"' || substr($m[2][0], 0, 1) == "'") {
2704
                            $output .= '[' . $m[2][0] . ']';
2705
                        } else {
2706
                            $output .= '["' . $m[2][0] . '"]';
2707
                        }
2708
                        array_shift($m[2]);
2709
                        array_shift($m[1]);
2710
                    }
2711
2712
                    if ($curBlock !== 'root') {
2713
                        $output = '(isset(' . $output . ') ? ' . $output . ':null)';
2714
                    }
2715
                }
2716
2717
                if (count($m[2])) {
2718
                    unset($m[0]);
2719
                    $output = '$this->readVarInto(' . str_replace("\n", '', var_export($m, true)) . ', ' . $output . ', ' . ($curBlock == 'root' ? 'false' : 'true') . ')';
2720
                }
2721
            }
2722
        } else {
2723
            preg_match_all('#(\[|->|\.)?((?:[a-z0-9_]|-(?!>))+)\]?#i', $key, $m);
2724
            unset($m[0]);
2725
            $output = '$this->readVar(' . str_replace("\n", '', var_export($m, true)) . ')';
2726
        }
2727
2728
        return $output;
2729
    }
2730
2731
    /**
2732
     * Flattens a variable tree, this helps in parsing very complex variables such as $var.foo[$foo.bar->baz].baz,
2733
     * it computes the contents of the brackets first and works out from there.
2734
     *
2735
     * @param array $tree     the variable tree parsed by he parseVar() method that must be flattened
2736
     * @param bool  $recursed leave that to false by default, it is only for internal use
2737
     *
2738
     * @return string flattened tree
2739
     */
2740
    protected function flattenVarTree(array $tree, $recursed = false)
2741
    {
2742
        $out = $recursed ? '".$this->readVarInto(' : '';
2743
        foreach ($tree as $bit) {
2744
            if (is_array($bit)) {
2745
                $out .= '.' . $this->flattenVarTree($bit, false);
2746
            } else {
2747
                $key = str_replace('"', '\\"', $bit);
2748
2749
                if (substr($key, 0, 1) === '$') {
2750
                    $out .= '".' . $this->parseVar($key, 0, strlen($key), false, 'variable') . '."';
2751
                } else {
2752
                    $cnt = substr_count($key, '$');
2753
2754
                    if ($this->debug) {
2755
                        echo 'PARSING SUBVARS IN : ' . $key . "\n";
2756
                    }
2757
                    if ($cnt > 0) {
2758
                        while (-- $cnt >= 0) {
2759
                            if (isset($last)) {
2760
                                $last = strrpos($key, '$', - (strlen($key) - $last + 1));
2761
                            } else {
2762
                                $last = strrpos($key, '$');
2763
                            }
2764
                            preg_match('#\$[a-z0-9_]+((?:(?:\.|->)(?:[a-z0-9_]+|(?R))|\[(?:[a-z0-9_]+|(?R))\]))*' . '((?:(?:[+/*%-])(?:\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))*)#i', substr($key, $last), $submatch);
2765
2766
                            $len = strlen($submatch[0]);
2767
                            $key = substr_replace(
2768
                                $key, preg_replace_callback(
2769
                                    '#(\$[a-z0-9_]+((?:(?:\.|->)(?:[a-z0-9_]+|(?R))|\[(?:[a-z0-9_]+|(?R))\]))*)' . '((?:(?:[+/*%-])(?:\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))*)#i', array(
2770
                                        $this,
2771
                                        'replaceVarKeyHelper'
2772
                                    ), substr($key, $last, $len)
2773
                                ), $last, $len
2774
                            );
2775
                            if ($this->debug) {
2776
                                echo 'RECURSIVE VAR REPLACEMENT DONE : ' . $key . "\n";
2777
                            }
2778
                        }
2779
                        unset($last);
2780
2781
                        $out .= $key;
2782
                    } else {
2783
                        $out .= $key;
2784
                    }
2785
                }
2786
            }
2787
        }
2788
        $out .= $recursed ? ', true)."' : '';
2789
2790
        return $out;
2791
    }
2792
2793
    /**
2794
     * Helper function that parses a variable.
2795
     *
2796
     * @param array $match the matched variable, array(1=>"string match")
2797
     *
2798
     * @return string parsed variable
2799
     */
2800
    protected function replaceVarKeyHelper($match)
2801
    {
2802
        return '".' . $this->parseVar($match[0], 0, strlen($match[0]), false, 'variable') . '."';
2803
    }
2804
2805
    /**
2806
     * Parses various constants, operators or non-quoted strings.
2807
     *
2808
     * @param string $in            the string within which we must parse something
2809
     * @param int    $from          the starting offset of the parsed area
2810
     * @param int    $to            the ending offset of the parsed area
2811
     * @param mixed  $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by
2812
     *                              default
2813
     * @param string $curBlock      the current parser-block being processed
2814
     * @param mixed  $pointer       a reference to a pointer that will be increased by the amount of characters parsed,
2815
     *                              or null by default
2816
     *
2817
     * @return string parsed values
2818
     * @throws Exception
2819
     */
2820
    protected function parseOthers($in, $from, $to, $parsingParams = false, $curBlock = '', &$pointer = null)
2821
    {
2822
        $substr = substr($in, $from, $to - $from);
2823
2824
        $end = strlen($substr);
2825
2826
        if ($curBlock === 'condition') {
2827
            $breakChars = array(
2828
                '(',
2829
                ')',
2830
                ' ',
2831
                '||',
2832
                '&&',
2833
                '|',
2834
                '&',
2835
                '>=',
2836
                '<=',
2837
                '===',
2838
                '==',
2839
                '=',
2840
                '!==',
2841
                '!=',
2842
                '<<',
2843
                '<',
2844
                '>>',
2845
                '>',
2846
                '^',
2847
                '~',
2848
                ',',
2849
                '+',
2850
                '-',
2851
                '*',
2852
                '/',
2853
                '%',
2854
                '!',
2855
                '?',
2856
                ':',
2857
                $this->rd,
2858
                ';'
2859
            );
2860
        } elseif ($curBlock === 'modifier') {
2861
            $breakChars = array(' ', ',', ')', ':', '|', "\r", "\n", "\t", ';', $this->rd);
2862
        } elseif ($curBlock === 'expression') {
2863
            $breakChars = array('/', '%', '+', '-', '*', ' ', ',', ')', "\r", "\n", "\t", ';', $this->rd);
2864
        } else {
2865
            $breakChars = array(' ', ',', ')', "\r", "\n", "\t", ';', $this->rd);
2866
        }
2867
2868
        $breaker = false;
2869
        while (list($k, $char) = each($breakChars)) {
2870
            $test = strpos($substr, $char);
2871
            if ($test !== false && $test < $end) {
2872
                $end     = $test;
2873
                $breaker = $k;
2874
            }
2875
        }
2876
2877
        if ($curBlock === 'condition') {
2878
            if ($end === 0 && $breaker !== false) {
2879
                $end = strlen($breakChars[$breaker]);
2880
            }
2881
        }
2882
2883
        if ($end !== false) {
2884
            $substr = substr($substr, 0, $end);
2885
        }
2886
2887
        if ($pointer !== null) {
2888
            $pointer += strlen($substr);
2889
        }
2890
2891
        $src    = $substr;
2892
        $substr = trim($substr);
2893
2894
        if (strtolower($substr) === 'false' || strtolower($substr) === 'no' || strtolower($substr) === 'off') {
2895
            if ($this->debug) {
2896
                echo 'BOOLEAN(FALSE) PARSED' . "\n";
2897
            }
2898
            $substr = 'false';
2899
            $type   = self::T_BOOL;
2900
        } elseif (strtolower($substr) === 'true' || strtolower($substr) === 'yes' || strtolower($substr) === 'on') {
2901
            if ($this->debug) {
2902
                echo 'BOOLEAN(TRUE) PARSED' . "\n";
2903
            }
2904
            $substr = 'true';
2905
            $type   = self::T_BOOL;
2906 View Code Duplication
        } elseif ($substr === 'null' || $substr === 'NULL') {
2907
            if ($this->debug) {
2908
                echo 'NULL PARSED' . "\n";
2909
            }
2910
            $substr = 'null';
2911
            $type   = self::T_NULL;
2912
        } elseif (is_numeric($substr)) {
2913
            $substr = (float)$substr;
2914
            if ((int)$substr == $substr) {
2915
                $substr = (int)$substr;
2916
            }
2917
            $type = self::T_NUMERIC;
2918
            if ($this->debug) {
2919
                echo 'NUMBER (' . $substr . ') PARSED' . "\n";
2920
            }
2921 View Code Duplication
        } elseif (preg_match('{^-?(\d+|\d*(\.\d+))\s*([/*%+-]\s*-?(\d+|\d*(\.\d+)))+$}', $substr)) {
2922
            if ($this->debug) {
2923
                echo 'SIMPLE MATH PARSED . "\n"';
2924
            }
2925
            $type   = self::T_MATH;
2926
            $substr = '(' . $substr . ')';
2927
        } elseif ($curBlock === 'condition' && array_search($substr, $breakChars, true) !== false) {
2928
            if ($this->debug) {
2929
                echo 'BREAKCHAR (' . $substr . ') PARSED' . "\n";
2930
            }
2931
            $type = self::T_BREAKCHAR;
2932
            //$substr = '"'.$substr.'"';
2933
        } else {
2934
            $substr = $this->replaceStringVars('\'' . str_replace('\'', '\\\'', $substr) . '\'', '\'', $curBlock);
2935
            $type   = self::T_UNQUOTED_STRING;
2936
            if ($this->debug) {
2937
                echo 'BLABBER (' . $substr . ') CASTED AS STRING' . "\n";
2938
            }
2939
        }
2940
2941
        if (is_array($parsingParams)) {
2942
            $parsingParams[] = array($substr, $src, $type);
2943
2944
            return $parsingParams;
2945
        } elseif ($curBlock === 'namedparam') {
2946
            return array($substr, $src, $type);
2947
        } elseif ($curBlock === 'expression') {
2948
            return $substr;
2949
        } else {
2950
            throw new Exception('Something went wrong');
2951
        }
2952
    }
2953
2954
    /**
2955
     * Replaces variables within a parsed string.
2956
     *
2957
     * @param string $string   the parsed string
2958
     * @param string $first    the first character parsed in the string, which is the string delimiter (' or ")
2959
     * @param string $curBlock the current parser-block being processed
2960
     *
2961
     * @return string the original string with variables replaced
2962
     */
2963
    protected function replaceStringVars($string, $first, $curBlock = '')
2964
    {
2965
        $pos = 0;
2966
        if ($this->debug) {
2967
            echo 'STRING VAR REPLACEMENT : ' . $string . "\n";
2968
        }
2969
        // replace vars
2970
        while (($pos = strpos($string, '$', $pos)) !== false) {
2971
            $prev = substr($string, $pos - 1, 1);
2972
            if ($prev === '\\') {
2973
                ++ $pos;
2974
                continue;
2975
            }
2976
2977
            $var = $this->parse($string, $pos, null, false, ($curBlock === 'modifier' ? 'modifier' : ($prev === '`' ? 'delimited_string' : 'string')));
2978
            $len = $var[0];
2979
            $var = $this->parse(str_replace('\\' . $first, $first, $string), $pos, null, false, ($curBlock === 'modifier' ? 'modifier' : ($prev === '`' ? 'delimited_string' : 'string')));
2980
2981
            if ($prev === '`' && substr($string, $pos + $len, 1) === '`') {
2982
                $string = substr_replace($string, $first . '.' . $var[1] . '.' . $first, $pos - 1, $len + 2);
2983
            } else {
2984
                $string = substr_replace($string, $first . '.' . $var[1] . '.' . $first, $pos, $len);
2985
            }
2986
            $pos += strlen($var[1]) + 2;
2987
            if ($this->debug) {
2988
                echo 'STRING VAR REPLACEMENT DONE : ' . $string . "\n";
2989
            }
2990
        }
2991
2992
        // handle modifiers
2993
        // TODO Obsolete?
2994
        $string = preg_replace_callback(
2995
            '#("|\')\.(.+?)\.\1((?:\|(?:@?[a-z0-9_]+(?:(?::("|\').+?\4|:[^`]*))*))+)#i', array(
2996
            $this,
2997
            'replaceModifiers'
2998
            ), $string
2999
        );
3000
3001
        // replace escaped dollar operators by unescaped ones if required
3002
        if ($first === "'") {
3003
            $string = str_replace('\\$', '$', $string);
3004
        }
3005
3006
        return $string;
3007
    }
3008
3009
    /**
3010
     * Replaces the modifiers applied to a string or a variable.
3011
     *
3012
     * @param array  $m        the regex matches that must be array(1=>"double or single quotes enclosing a string,
3013
     *                         when applicable", 2=>"the string or var", 3=>"the modifiers matched")
3014
     * @param string $curBlock the current parser-block being processed
3015
     * @param null   $pointer
3016
     *
3017
     * @return string the input enclosed with various function calls according to the modifiers found
3018
     * @throws CompilationException
3019
     * @throws Exception
3020
     */
3021
    protected function replaceModifiers(array $m, $curBlock = null, &$pointer = null)
3022
    {
3023
        if ($this->debug) {
3024
            echo 'PARSING MODIFIERS : ' . $m[3] . "\n";
3025
        }
3026
3027
        if ($pointer !== null) {
3028
            $pointer += strlen($m[3]);
3029
        }
3030
        // remove first pipe
3031
        $cmdstrsrc = substr($m[3], 1);
3032
        // remove last quote if present
3033
        if (substr($cmdstrsrc, - 1, 1) === $m[1]) {
3034
            $cmdstrsrc = substr($cmdstrsrc, 0, - 1);
3035
            $add       = $m[1];
3036
        }
3037
3038
        $output = $m[2];
3039
3040
        $continue = true;
3041
        while (strlen($cmdstrsrc) > 0 && $continue) {
3042
            if ($cmdstrsrc[0] === '|') {
3043
                $cmdstrsrc = substr($cmdstrsrc, 1);
3044
                continue;
3045
            }
3046
            if ($cmdstrsrc[0] === ' ' || $cmdstrsrc[0] === ';' || substr($cmdstrsrc, 0, strlen($this->rd)) === $this->rd) {
3047
                if ($this->debug) {
3048
                    echo 'MODIFIER PARSING ENDED, RIGHT DELIMITER or ";" FOUND' . "\n";
3049
                }
3050
                $continue = false;
3051
                if ($pointer !== null) {
3052
                    $pointer -= strlen($cmdstrsrc);
3053
                }
3054
                break;
3055
            }
3056
            $cmdstr   = $cmdstrsrc;
3057
            $paramsep = ':';
3058 View Code Duplication
            if (!preg_match('/^(@{0,2}[a-z_][a-z0-9_]*)(:)?/i', $cmdstr, $match)) {
3059
                throw new CompilationException($this, 'Invalid modifier name, started with : ' . substr($cmdstr, 0, 10));
3060
            }
3061
            $paramspos = !empty($match[2]) ? strlen($match[1]) : false;
3062
            $func      = $match[1];
3063
3064
            $state = 0;
3065
            if ($paramspos === false) {
3066
                $cmdstrsrc = substr($cmdstrsrc, strlen($func));
3067
                $params    = array();
3068
                if ($this->debug) {
3069
                    echo 'MODIFIER (' . $func . ') CALLED WITH NO PARAMS' . "\n";
3070
                }
3071
            } else {
3072
                $paramstr = substr($cmdstr, $paramspos + 1);
3073 View Code Duplication
                if (substr($paramstr, - 1, 1) === $paramsep) {
3074
                    $paramstr = substr($paramstr, 0, - 1);
3075
                }
3076
3077
                $ptr    = 0;
3078
                $params = array();
3079
                while ($ptr < strlen($paramstr)) {
3080
                    if ($this->debug) {
3081
                        echo 'MODIFIER (' . $func . ') START PARAM PARSING WITH POINTER AT ' . $ptr . "\n";
3082
                    }
3083
                    if ($this->debug) {
3084
                        echo $paramstr . '--' . $ptr . '--' . strlen($paramstr) . '--modifier' . "\n";
3085
                    }
3086
                    $params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'modifier', $ptr);
3087
                    if ($this->debug) {
3088
                        echo 'PARAM PARSED, POINTER AT ' . $ptr . "\n";
3089
                    }
3090
3091
                    if ($ptr >= strlen($paramstr)) {
3092
                        if ($this->debug) {
3093
                            echo 'PARAM PARSING ENDED, PARAM STRING CONSUMED' . "\n";
3094
                        }
3095
                        break;
3096
                    }
3097
3098
                    if ($paramstr[$ptr] === ' ' || $paramstr[$ptr] === '|' || $paramstr[$ptr] === ';' || substr($paramstr, $ptr, strlen($this->rd)) === $this->rd) {
3099
                        if ($this->debug) {
3100
                            echo 'PARAM PARSING ENDED, " ", "|", RIGHT DELIMITER or ";" FOUND, POINTER AT ' . $ptr . "\n";
3101
                        }
3102
                        if ($paramstr[$ptr] !== '|') {
3103
                            $continue = false;
3104
                            if ($pointer !== null) {
3105
                                $pointer -= strlen($paramstr) - $ptr;
3106
                            }
3107
                        }
3108
                        ++ $ptr;
3109
                        break;
3110
                    }
3111
                    if ($ptr < strlen($paramstr) && $paramstr[$ptr] === ':') {
3112
                        ++ $ptr;
3113
                    }
3114
                }
3115
                $cmdstrsrc = substr($cmdstrsrc, strlen($func) + 1 + $ptr);
3116
                foreach ($params as $k => $p) {
3117
                    if (is_array($p) && is_array($p[1])) {
3118
                        $state |= 2;
3119
                    } else {
3120
                        if (($state & 2) && preg_match('#^(["\'])(.+?)\1$#', $p[0], $m)) {
3121
                            $params[$k] = array($m[2], array('true', 'true'));
3122
                        } else {
3123
                            if ($state & 2) {
3124
                                throw new CompilationException($this, 'You can not use an unnamed parameter after a named one');
3125
                            }
3126
                            $state |= 1;
3127
                        }
3128
                    }
3129
                }
3130
            }
3131
3132
            // check if we must use array_map with this plugin or not
3133
            $mapped = false;
3134
            if (substr($func, 0, 1) === '@') {
3135
                $func   = substr($func, 1);
3136
                $mapped = true;
3137
            }
3138
3139
            $pluginType = $this->getPluginType($func);
3140
3141
            if ($state & 2) {
3142
                array_unshift($params, array('value', is_array($output) ? $output : array($output, $output)));
3143
            } else {
3144
                array_unshift($params, is_array($output) ? $output : array($output, $output));
3145
            }
3146
3147
            if ($pluginType & Core::NATIVE_PLUGIN) {
3148
                $params = $this->mapParams($params, null, $state);
3149
3150
                $params = $params['*'][0];
3151
3152
                $params = self::implode_r($params);
3153
3154 View Code Duplication
                if ($mapped) {
3155
                    $output = '$this->arrayMap(\'' . $func . '\', array(' . $params . '))';
3156
                } else {
3157
                    $output = $func . '(' . $params . ')';
3158
                }
3159
            } elseif ($pluginType & Core::PROXY_PLUGIN) {
3160
                $params = $this->mapParams($params, $this->getDwoo()->getPluginProxy()->getCallback($func), $state);
3161
                foreach ($params as &$p) {
3162
                    $p = $p[0];
3163
                }
3164
                $output = call_user_func(array($this->getDwoo()->getPluginProxy(), 'getCode'), $func, $params);
3165
            } elseif ($pluginType & Core::SMARTY_MODIFIER) {
3166
                $params = $this->mapParams($params, null, $state);
3167
                $params = $params['*'][0];
3168
3169
                $params = self::implode_r($params);
3170
3171
                if ($pluginType & Core::CUSTOM_PLUGIN) {
3172
                    $callback = $this->customPlugins[$func]['callback'];
3173
                    if (is_array($callback)) {
3174
                        if (is_object($callback[0])) {
3175
                            $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array($this->plugins[\'' . $func . '\'][\'callback\'][0], \'' . $callback[1] . '\'), array(' . $params . '))';
3176
                        } else {
3177
                            $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), array(' . $params . '))';
3178
                        }
3179
                    } elseif ($mapped) {
3180
                        $output = '$this->arrayMap(\'' . $callback . '\', array(' . $params . '))';
3181
                    } else {
3182
                        $output = $callback . '(' . $params . ')';
3183
                    }
3184
                } elseif ($mapped) {
3185
                    $output = '$this->arrayMap(\'smarty_modifier_' . $func . '\', array(' . $params . '))';
3186
                } else {
3187
                    $output = 'smarty_modifier_' . $func . '(' . $params . ')';
3188
                }
3189
            } else {
3190
                if ($pluginType & Core::CUSTOM_PLUGIN) {
3191
                    $callback   = $this->customPlugins[$func]['callback'];
3192
                    $pluginName = $callback;
3193
                } else {
3194
                    if (class_exists('Plugin' . Core::toCamelCase($func)) !== false || function_exists('Plugin' .
3195
                            Core::toCamelCase($func) . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : ''))
3196
                        !== false) {
3197
                        $pluginName = 'Plugin' . Core::toCamelCase($func);
3198
                    } else {
3199
                        $pluginName = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func);
3200
                    }
3201
                    if ($pluginType & Core::CLASS_PLUGIN) {
3202
                        $callback = array($pluginName, ($pluginType & Core::COMPILABLE_PLUGIN) ? 'compile' : 'process');
3203
                    } else {
3204
                        $callback = $pluginName . (($pluginType & Core::COMPILABLE_PLUGIN) ? 'Compile' : '');
3205
                    }
3206
                }
3207
                $params = $this->mapParams($params, $callback, $state);
3208
3209
                foreach ($params as &$p) {
3210
                    $p = $p[0];
3211
                }
3212
3213
                if ($pluginType & Core::FUNC_PLUGIN) {
3214
                    if ($pluginType & Core::COMPILABLE_PLUGIN) {
3215
                        if ($mapped) {
3216
                            throw new CompilationException($this, 'The @ operator can not be used on compiled plugins.');
3217
                        }
3218
                        if ($pluginType & Core::CUSTOM_PLUGIN) {
3219
                            $funcCompiler = $this->customPlugins[$func]['callback'];
3220
                        } else {
3221
                            if (function_exists('Plugin' . Core::toCamelCase($func) . 'Compile') !== false) {
3222
                                $funcCompiler = 'Plugin' . Core::toCamelCase($func) . 'Compile';
3223
                            } else {
3224
                                $funcCompiler = Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) .
3225
                                    'Compile';
3226
                            }
3227
                        }
3228
                        array_unshift($params, $this);
3229
                        $output = call_user_func_array($funcCompiler, $params);
3230
                    } else {
3231
                        array_unshift($params, '$this');
3232
3233
                        $params = self::implode_r($params);
3234 View Code Duplication
                        if ($mapped) {
3235
                            $output = '$this->arrayMap(\'' . $pluginName . '\', array(' . $params . '))';
3236
                        } else {
3237
                            $output = $pluginName . '(' . $params . ')';
3238
                        }
3239
                    }
3240
                } else {
3241
                    if ($pluginType & Core::COMPILABLE_PLUGIN) {
3242
                        if ($mapped) {
3243
                            throw new CompilationException($this, 'The @ operator can not be used on compiled plugins.');
3244
                        }
3245 View Code Duplication
                        if ($pluginType & Core::CUSTOM_PLUGIN) {
3246
                            $callback = $this->customPlugins[$func]['callback'];
3247
                            if (!is_array($callback)) {
3248
                                if (!method_exists($callback, 'compile')) {
3249
                                    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');
3250
                                }
3251
                                if (($ref = new ReflectionMethod($callback, 'compile')) && $ref->isStatic()) {
3252
                                    $funcCompiler = array($callback, 'compile');
3253
                                } else {
3254
                                    $funcCompiler = array(new $callback(), 'compile');
3255
                                }
3256
                            } else {
3257
                                $funcCompiler = $callback;
3258
                            }
3259
                        } else {
3260
                            if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
3261
                                $funcCompiler = array('Plugin' . Core::toCamelCase($func), 'compile');
3262
                            } else {
3263
                                $funcCompiler = array(
3264
                                    Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func),
3265
                                    'compile'
3266
                                );
3267
                            }
3268
                            array_unshift($params, $this);
3269
                        }
3270
                        $output = call_user_func_array($funcCompiler, $params);
3271
                    } else {
3272
                        $params = self::implode_r($params);
3273
3274
                        if ($pluginType & Core::CUSTOM_PLUGIN) {
3275
                            if (is_object($callback[0])) {
3276
                                $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array($this->plugins[\'' . $func . '\'][\'callback\'][0], \'' . $callback[1] . '\'), array(' . $params . '))';
3277
                            } else {
3278
                                $output = ($mapped ? '$this->arrayMap' : 'call_user_func_array') . '(array(\'' . $callback[0] . '\', \'' . $callback[1] . '\'), array(' . $params . '))';
3279
                            }
3280 View Code Duplication
                        } elseif ($mapped) {
3281
                            $output = '$this->arrayMap(array($this->getObjectPlugin(\''.
3282
                                Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($func) . '\'), 
3283
                            \'process\'), array(' . $params . '))';
3284
                        } else {
3285
                            if (class_exists('Plugin' . Core::toCamelCase($func)) !== false) {
3286
                                $output = '$this->classCall(\'Plugin' . Core::toCamelCase($func) . '\', array(' . $params . '))';
3287
                            } else {
3288
                                $output = '$this->classCall(\'' . $func . '\', array(' . $params . '))';
3289
                            }
3290
                        }
3291
                    }
3292
                }
3293
            }
3294
        }
3295
3296
        if ($curBlock === 'namedparam') {
3297
            return array($output, $output);
3298
        } elseif ($curBlock === 'var' || $m[1] === null) {
3299
            return $output;
3300
        } elseif ($curBlock === 'string' || $curBlock === 'root') {
3301
            return $m[1] . '.' . $output . '.' . $m[1] . (isset($add) ? $add : null);
3302
        }
3303
3304
        return '';
3305
    }
3306
3307
    /**
3308
     * Recursively implodes an array in a similar manner as var_export() does but with some tweaks
3309
     * to handle pre-compiled values and the fact that we do not need to enclose everything with
3310
     * "array" and do not require top-level keys to be displayed.
3311
     *
3312
     * @param array $params        the array to implode
3313
     * @param bool  $recursiveCall if set to true, the function outputs key names for the top level
3314
     *
3315
     * @return string the imploded array
3316
     */
3317
    public static function implode_r(array $params, $recursiveCall = false)
3318
    {
3319
        $out = '';
3320
        foreach ($params as $k => $p) {
3321
            if (is_array($p)) {
3322
                $out2 = 'array(';
3323
                foreach ($p as $k2 => $v) {
3324
                    $out2 .= var_export($k2, true) . ' => ' . (is_array($v) ? 'array(' . self::implode_r($v, true) . ')' : $v) . ', ';
3325
                }
3326
                $p = rtrim($out2, ', ') . ')';
3327
            }
3328
            if ($recursiveCall) {
3329
                $out .= var_export($k, true) . ' => ' . $p . ', ';
3330
            } else {
3331
                $out .= $p . ', ';
3332
            }
3333
        }
3334
3335
        return rtrim($out, ', ');
3336
    }
3337
3338
    /**
3339
     * Returns the plugin type of a plugin and adds it to the used plugins array if required.
3340
     *
3341
     * @param string $name plugin name, as found in the template
3342
     *
3343
     * @return int type as a multi bit flag composed of the Dwoo plugin types constants
3344
     * @throws Exception
3345
     * @throws SecurityException
3346
     * @throws Exception
3347
     */
3348
    protected function getPluginType($name)
3349
    {
3350
        $pluginType = - 1;
3351
3352
        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)) {
3353
            $phpFunc = true;
3354
        } elseif ($this->securityPolicy !== null && function_exists($name) && array_key_exists(strtolower($name), $this->securityPolicy->getAllowedPhpFunctions()) === false) {
3355
            throw new SecurityException('Call to a disallowed php function : ' . $name);
3356
        }
3357
3358
        while ($pluginType <= 0) {
3359
            // Template plugin compilable
3360
            if (isset($this->templatePlugins[$name])) {
3361
                $pluginType = Core::TEMPLATE_PLUGIN | Core::COMPILABLE_PLUGIN;
3362
            } // Custom plugin
3363
            elseif (isset($this->customPlugins[$name])) {
3364
                $pluginType = $this->customPlugins[$name]['type'] | Core::CUSTOM_PLUGIN;
3365
            } // Class blocks plugin
3366
            elseif (class_exists(Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($name), false) !==
3367
                false) {
3368
                if (is_subclass_of(Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($name), 'Dwoo\Block\Plugin')) {
3369
                    $pluginType = Core::BLOCK_PLUGIN;
3370
                } else {
3371
                    $pluginType = Core::CLASS_PLUGIN;
3372
                }
3373
                $interfaces = class_implements(Core::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . Core::toCamelCase($name));
3374 View Code Duplication
                if (in_array('Dwoo\ICompilable', $interfaces) !== false || in_array('Dwoo\ICompilable\Block', $interfaces) !== false) {
3375
                    $pluginType |= Core::COMPILABLE_PLUGIN;
3376
                }
3377
            } // Class functions plugin
3378
            elseif(class_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($name), false) !==
3379
                false) {
3380
                $pluginType = Core::CLASS_PLUGIN;
3381
                $interfaces = class_implements(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($name));
3382 View Code Duplication
                if (in_array('Dwoo\ICompilable', $interfaces) !== false || in_array('Dwoo\ICompilable\Block', $interfaces) !== false) {
3383
                    $pluginType |= Core::COMPILABLE_PLUGIN;
3384
                }
3385
            } // Class without namespace
3386
            elseif(class_exists('Plugin' . Core::toCamelCase($name), false) !== false) {
3387
                $pluginType = Core::CLASS_PLUGIN;
3388
                $interfaces = class_implements('Plugin' . Core::toCamelCase($name));
3389 View Code Duplication
                if (in_array('Dwoo\ICompilable', $interfaces) !== false || in_array('Dwoo\ICompilable\Block', $interfaces) !== false) {
3390
                    $pluginType |= Core::COMPILABLE_PLUGIN;
3391
                }
3392
            } // Function plugin (with/without namespaces)
3393
            elseif (function_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase
3394
                    ($name)) !==
3395
                false || function_exists('Plugin' . Core::toCamelCase($name)) !== false) {
3396
                $pluginType = Core::FUNC_PLUGIN;
3397
            } // Function plugin compile (with/without namespaces)
3398
            elseif (function_exists(Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin' . Core::toCamelCase($name) .
3399
                    'Compile') !== false || function_exists('Plugin' . Core::toCamelCase($name) . 'Compile') !==
3400
                false) {
3401
                $pluginType = Core::FUNC_PLUGIN | Core::COMPILABLE_PLUGIN;
3402
            } // Helper plugin compile
3403
            elseif(function_exists(Core::NAMESPACE_PLUGINS_HELPERS . 'Plugin' . Core::toCamelCase($name) . 'Compile')
3404
                !== false) {
3405
                $pluginType = Core::FUNC_PLUGIN | Core::COMPILABLE_PLUGIN;
3406
            } // Smarty modifier
3407
            elseif (function_exists('smarty_modifier_' . $name) !== false) {
3408
                $pluginType = Core::SMARTY_MODIFIER;
3409
            } // Smarty function
3410
            elseif (function_exists('smarty_function_' . $name) !== false) {
3411
                $pluginType = Core::SMARTY_FUNCTION;
3412
            } // Smarty block
3413
            elseif (function_exists('smarty_block_' . $name) !== false) {
3414
                $pluginType = Core::SMARTY_BLOCK;
3415
            } // Everything else
3416
            else {
3417
                if ($pluginType === - 1) {
3418
                    try {
3419
                        $this->getDwoo()->getLoader()->loadPlugin(
3420
                            'Plugin' . Core::toCamelCase($name));
3421
                    }
3422
                    catch (Exception $e) {
3423
                        if (isset($phpFunc)) {
3424
                            $pluginType = Core::NATIVE_PLUGIN;
3425
                        } elseif (is_object($this->getDwoo()->getPluginProxy()) && $this->getDwoo()->getPluginProxy()->handles($name)) {
3426
                            $pluginType = Core::PROXY_PLUGIN;
3427
                            break;
3428
                        } else {
3429
                            throw $e;
3430
                        }
3431
                    }
3432
                } else {
3433
                    throw new Exception('Plugin "' . $name . '" could not be found, type:' . $pluginType);
3434
                }
3435
                ++ $pluginType;
3436
            }
3437
        }
3438
3439
        if (($pluginType & Core::COMPILABLE_PLUGIN) === 0 && ($pluginType & Core::NATIVE_PLUGIN) === 0 && ($pluginType & Core::PROXY_PLUGIN) === 0) {
3440
            $this->addUsedPlugin(Core::toCamelCase($name), $pluginType);
3441
        }
3442
3443
        return $pluginType;
3444
    }
3445
3446
    /**
3447
     * Allows a plugin to load another one at compile time, this will also mark
3448
     * it as used by this template so it will be loaded at runtime (which can be
3449
     * useful for compiled plugins that rely on another plugin when their compiled
3450
     * code runs).
3451
     *
3452
     * @param string $name the plugin name
3453
     *
3454
     * @return void
3455
     */
3456
    public function loadPlugin($name)
3457
    {
3458
        $this->getPluginType($name);
3459
    }
3460
3461
    /**
3462
     * Runs htmlentities over the matched <?php ?> blocks when the security policy enforces that.
3463
     *
3464
     * @param array $match matched php block
3465
     *
3466
     * @return string the htmlentities-converted string
3467
     */
3468
    protected function phpTagEncodingHelper($match)
3469
    {
3470
        return htmlspecialchars($match[0]);
3471
    }
3472
3473
    /**
3474
     * Maps the parameters received from the template onto the parameters required by the given callback.
3475
     *
3476
     * @param array    $params   the array of parameters
3477
     * @param callback $callback the function or method to reflect on to find out the required parameters
3478
     * @param int      $callType the type of call in the template, 0 = no params, 1 = php-style call, 2 = named
3479
     *                           parameters call
3480
     * @param array    $map      the parameter map to use, if not provided it will be built from the callback
3481
     *
3482
     * @return array parameters sorted in the correct order with missing optional parameters filled
3483
     * @throws CompilationException
3484
     */
3485
    protected function mapParams(array $params, $callback, $callType = 2, $map = null)
3486
    {
3487
        if (!$map) {
3488
            $map = $this->getParamMap($callback);
3489
        }
3490
3491
        $paramlist = array();
3492
3493
        // transforms the parameter array from (x=>array('paramname'=>array(values))) to (paramname=>array(values))
3494
        $ps = array();
3495
        foreach ($params as $p) {
3496
            if (is_array($p[1])) {
3497
                $ps[$p[0]] = $p[1];
3498
            } else {
3499
                $ps[] = $p;
3500
            }
3501
        }
3502
3503
        // loops over the param map and assigns values from the template or default value for unset optional params
3504
        while (list($k, $v) = each($map)) {
3505
            if ($v[0] === '*') {
3506
                // "rest" array parameter, fill every remaining params in it and then break
3507
                if (count($ps) === 0) {
3508
                    if ($v[1] === false) {
3509
                        throw new CompilationException(
3510
                            $this, 'Rest argument missing for ' . str_replace(
3511
                                array(
3512
                                    Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin',
3513
                                'Compile'
3514
                                ), '', (is_array($callback) ? $callback[0] : $callback)
3515
                            )
3516
                        );
3517
                    } else {
3518
                        break;
3519
                    }
3520
                }
3521
                $tmp  = array();
3522
                $tmp2 = array();
3523
                $tmp3 = array();
3524
                foreach ($ps as $i => $p) {
3525
                    $tmp[$i]  = $p[0];
3526
                    $tmp2[$i] = $p[1];
3527
                    $tmp3[$i] = isset($p[2]) ? $p[2] : 0;
3528
                    unset($ps[$i]);
3529
                }
3530
                $paramlist[$v[0]] = array($tmp, $tmp2, $tmp3);
3531
                unset($tmp, $tmp2, $i, $p);
3532
                break;
3533
            } elseif (isset($ps[$v[0]])) {
3534
                // parameter is defined as named param
3535
                $paramlist[$v[0]] = $ps[$v[0]];
3536
                unset($ps[$v[0]]);
3537
            } elseif (isset($ps[$k])) {
3538
                // parameter is defined as ordered param
3539
                $paramlist[$v[0]] = $ps[$k];
3540
                unset($ps[$k]);
3541
            } elseif ($v[1] === false) {
3542
                // parameter is not defined and not optional, throw error
3543
                if (is_array($callback)) {
3544
                    if (is_object($callback[0])) {
3545
                        $name = get_class($callback[0]) . '::' . $callback[1];
3546
                    } else {
3547
                        $name = $callback[0];
3548
                    }
3549
                } else {
3550
                    $name = $callback;
3551
                }
3552
3553
                throw new CompilationException(
3554
                    $this, 'Argument ' . $k . '/' . $v[0] . ' missing for ' . str_replace(
3555
                        array(
3556
                            Core::NAMESPACE_PLUGINS_FUNCTIONS . 'Plugin',
3557
                        'Compile'
3558
                        ), '', $name
3559
                    )
3560
                );
3561
            } elseif ($v[2] === null) {
3562
                // enforce lowercased null if default value is null (php outputs NULL with var export)
3563
                $paramlist[$v[0]] = array('null', null, self::T_NULL);
3564
            } else {
3565
                // outputs default value with var_export
3566
                $paramlist[$v[0]] = array(var_export($v[2], true), $v[2]);
3567
            }
3568
        }
3569
3570
        if (count($ps)) {
3571
            foreach ($ps as $i => $p) {
3572
                array_push($paramlist, $p);
3573
            }
3574
        }
3575
3576
        return $paramlist;
3577
    }
3578
3579
    /**
3580
     * Returns the parameter map of the given callback, it filters out entries typed as Dwoo and Compiler and turns the
3581
     * rest parameter into a "*".
3582
     *
3583
     * @param callback $callback the function/method to reflect on
3584
     *
3585
     * @return array processed parameter map
3586
     */
3587
    protected function getParamMap($callback)
3588
    {
3589
        if (is_null($callback)) {
3590
            return array(array('*', true));
3591
        }
3592
        if (is_array($callback)) {
3593
            $ref = new ReflectionMethod($callback[0], $callback[1]);
3594
        } else {
3595
            $ref = new ReflectionFunction($callback);
3596
        }
3597
3598
        $out = array();
3599
        foreach ($ref->getParameters() as $param) {
3600
            if (($class = $param->getClass()) !== null && $class->name === 'Dwoo\Core') {
3601
                continue;
3602
            }
3603
            if (($class = $param->getClass()) !== null && $class->name === 'Dwoo\Compiler') {
3604
                continue;
3605
            }
3606
            if ($param->getName() === 'rest' && $param->isArray() === true) {
3607
                $out[] = array('*', $param->isOptional(), null);
3608
                continue;
3609
            }
3610
            $out[] = array(
3611
                $param->getName(),
3612
                $param->isOptional(),
3613
                $param->isOptional() ? $param->getDefaultValue() : null
3614
            );
3615
        }
3616
3617
        return $out;
3618
    }
3619
3620
    /**
3621
     * Returns a default instance of this compiler, used by default by all Dwoo templates that do not have a
3622
     * specific compiler assigned and when you do not override the default compiler factory function.
3623
     *
3624
     * @see    Core::setDefaultCompilerFactory()
3625
     * @return Compiler
3626
     */
3627
    public static function compilerFactory()
3628
    {
3629
        if (self::$instance === null) {
3630
            self::$instance = new self();
3631
        }
3632
3633
        return self::$instance;
3634
    }
3635
}
3636