Issues (292)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

lib/Dwoo/Core.php (1 issue)

assigning incompatible types to properties.

Bug Documentation Major

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/**
3
 * Copyright (c) 2013-2017
4
 *
5
 * @category  Library
6
 * @package   Dwoo
7
 * @author    Jordi Boggiano <[email protected]>
8
 * @author    David Sanchez <[email protected]>
9
 * @copyright 2008-2013 Jordi Boggiano
10
 * @copyright 2013-2017 David Sanchez
11
 * @license   http://dwoo.org/LICENSE LGPLv3
12
 * @version   1.3.6
13
 * @date      2017-03-23
14
 * @link      http://dwoo.org/
15
 */
16
17
namespace Dwoo;
18
19
use ArrayAccess;
20
use Closure;
21
use Countable;
22
use Dwoo\Plugins\Blocks\PluginDynamic;
23
use Dwoo\Security\Policy as SecurityPolicy;
24
use Dwoo\Block\Plugin as BlockPlugin;
25
use Dwoo\Template\File as TemplateFile;
26
use Iterator;
27
use stdClass;
28
use Traversable;
29
30
/**
31
 * Main dwoo class, allows communication between the compiler, template and data classes.
32
 * <pre>
33
 * requirements :
34
 *  php 5.3.0 or above (might work below, it's a rough estimate)
35
 *  SPL and PCRE extensions (for php versions prior to 5.3.0)
36
 *  mbstring extension for some string manipulation plugins (especially if you intend to use UTF-8)
37
 * recommended :
38
 *  hash extension (for Dwoo\Template\Str - minor performance boost)
39
 * project created :
40
 *  2008-01-05
41
 * </pre>
42
 * This software is provided 'as-is', without any express or implied warranty.
43
 * In no event will the authors be held liable for any damages arising from the use of this software.
44
 */
45
class Core
46
{
47
    /**
48
     * Current version number.
49
     *
50
     * @var string
51
     */
52
    const VERSION = '1.3.6';
53
54
    /**
55
     * Unique number of this dwoo release, based on version number.
56
     * this can be used by templates classes to check whether the compiled template
57
     * has been compiled before this release or not, so that old templates are
58
     * recompiled automatically when Dwoo is updated
59
     */
60
    const RELEASE_TAG = 136;
61
62
    /**
63
     * Constants that represents all plugin types
64
     * these are bitwise-operation-safe values to allow multiple types
65
     * on a single plugin
66
     *
67
     * @var int
68
     */
69
    const CLASS_PLUGIN      = 1;
70
    const FUNC_PLUGIN       = 2;
71
    const NATIVE_PLUGIN     = 4;
72
    const BLOCK_PLUGIN      = 8;
73
    const COMPILABLE_PLUGIN = 16;
74
    const CUSTOM_PLUGIN     = 32;
75
    const SMARTY_MODIFIER   = 64;
76
    const SMARTY_BLOCK      = 128;
77
    const SMARTY_FUNCTION   = 256;
78
    const PROXY_PLUGIN      = 512;
79
    const TEMPLATE_PLUGIN   = 1024;
80
81
    /**
82
     * Constant to default namespaces of builtin plugins
83
     *
84
     * @var string
85
     */
86
    const NAMESPACE_PLUGINS_BLOCKS     = 'Dwoo\Plugins\Blocks\\';
87
    const NAMESPACE_PLUGINS_FILTERS    = 'Dwoo\Plugins\Filters\\';
88
    const NAMESPACE_PLUGINS_FUNCTIONS  = 'Dwoo\Plugins\Functions\\';
89
    const NAMESPACE_PLUGINS_HELPERS    = 'Dwoo\Plugins\Helpers\\';
90
    const NAMESPACE_PLUGINS_PROCESSORS = 'Dwoo\Plugins\Processors\\';
91
92
    /**
93
     * Character set of the template, used by string manipulation plugins.
94
     * it must be lowercase, but setCharset() will take care of that
95
     *
96
     * @see setCharset
97
     * @see getCharset
98
     * @var string
99
     */
100
    protected $charset = 'UTF-8';
101
102
    /**
103
     * Global variables that are accessible through $dwoo.* in the templates.
104
     * default values include:
105
     * $dwoo.version - current version number
106
     * $dwoo.ad - a Powered by Dwoo link pointing to dwoo.org
107
     * $dwoo.now - the current time
108
     * $dwoo.template - the current template filename
109
     * $dwoo.charset - the character set used by the template
110
     * on top of that, foreach and other plugins can store special values in there,
111
     * see their documentation for more details.
112
     *
113
     * @var array
114
     */
115
    protected $globals = array();
116
117
    /**
118
     * Directory where the compiled templates are stored.
119
     * defaults to DWOO_COMPILEDIR (= dwoo_dir/compiled by default)
120
     *
121
     * @var string
122
     */
123
    protected $compileDir;
124
125
    /**
126
     * Directory where the cached templates are stored.
127
     * defaults to DWOO_CACHEDIR (= dwoo_dir/cache by default)
128
     *
129
     * @var string
130
     */
131
    protected $cacheDir;
132
133
    /**
134
     * Directory where the template files are stored
135
     *
136
     * @var array
137
     */
138
    protected $templateDir = array();
139
140
    /**
141
     * Defines how long (in seconds) the cached files must remain valid.
142
     * can be overridden on a per-template basis
143
     * -1 = never delete
144
     * 0 = disabled
145
     * >0 = duration in seconds
146
     *
147
     * @var int
148
     */
149
    protected $cacheTime = 0;
150
151
    /**
152
     * Security policy object.
153
     *
154
     * @var SecurityPolicy
155
     */
156
    protected $securityPolicy = null;
157
158
    /**
159
     * Stores the custom plugins callbacks.
160
     *
161
     * @see addPlugin
162
     * @see removePlugin
163
     * @var array
164
     */
165
    protected $plugins = array();
166
167
    /**
168
     * Stores the filter callbacks.
169
     *
170
     * @see addFilter
171
     * @see removeFilter
172
     * @var array
173
     */
174
    protected $filters = array();
175
176
    /**
177
     * Stores the resource types and associated
178
     * classes / compiler classes.
179
     *
180
     * @var array
181
     */
182
    protected $resources = array(
183
        'file'   => array(
184
            'class'    => 'Dwoo\Template\File',
185
            'compiler' => null,
186
        ),
187
        'string' => array(
188
            'class'    => 'Dwoo\Template\Str',
189
            'compiler' => null,
190
        ),
191
    );
192
193
    /**
194
     * The dwoo loader object used to load plugins by this dwoo instance.
195
     *
196
     * @var ILoader
197
     */
198
    protected $loader = null;
199
200
    /**
201
     * Currently rendered template, set to null when not-rendering.
202
     *
203
     * @var ITemplate
204
     */
205
    protected $template = null;
206
207
    /**
208
     * Stores the instances of the class plugins during template runtime.
209
     *
210
     * @var array
211
     */
212
    protected $runtimePlugins = array();
213
214
    /**
215
     * Stores the returned values during template runtime.
216
     *
217
     * @var array
218
     */
219
    protected $returnData = array();
220
221
    /**
222
     * Stores the data during template runtime.
223
     *
224
     * @var array
225
     */
226
    protected $data = array();
227
228
    /**
229
     * Stores the current scope during template runtime.
230
     * this should ideally not be accessed directly from outside template code
231
     *
232
     * @var mixed
233
     */
234
    public $scope;
235
236
    /**
237
     * Stores the scope tree during template runtime.
238
     *
239
     * @var array
240
     */
241
    protected $scopeTree = array();
242
243
    /**
244
     * Stores the block plugins stack during template runtime.
245
     *
246
     * @var array
247
     */
248
    protected $stack = array();
249
250
    /**
251
     * Stores the current block plugin at the top of the stack during template runtime.
252
     *
253
     * @var BlockPlugin
254
     */
255
    protected $curBlock;
256
257
    /**
258
     * Stores the output buffer during template runtime.
259
     *
260
     * @var string
261
     */
262
    protected $buffer;
263
264
    /**
265
     * Stores plugin proxy.
266
     *
267
     * @var IPluginProxy
268
     */
269
    protected $pluginProxy;
270
271
    /**
272
     * Constructor, sets the cache and compile dir to the default values if not provided.
273
     *
274
     * @param string $compileDir path to the compiled directory, defaults to lib/compiled
275
     * @param string $cacheDir   path to the cache directory, defaults to lib/cache
276
     */
277
    public function __construct($compileDir = null, $cacheDir = null)
278
    {
279
        if ($compileDir !== null) {
280
            $this->setCompileDir($compileDir);
281
        }
282
        if ($cacheDir !== null) {
283
            $this->setCacheDir($cacheDir);
284
        }
285
        $this->initGlobals();
286
    }
287
288
    /**
289
     * Resets some runtime variables to allow a cloned object to be used to render sub-templates.
290
     *
291
     * @return void
292
     */
293
    public function __clone()
294
    {
295
        $this->template = null;
296
        unset($this->data);
297
        unset($this->returnData);
298
    }
299
300
    /**
301
     * Returns the given template rendered using the provided data and optional compiler.
302
     *
303
     * @param mixed     $_tpl      template, can either be a ITemplate object (i.e. TemplateFile), a
304
     *                             valid path to a template, or a template as a string it is recommended to
305
     *                             provide a ITemplate as it will probably make things faster, especially if
306
     *                             you render a template multiple times
307
     * @param mixed     $data      the data to use, can either be a IDataProvider object (i.e. Data) or
308
     *                             an associative array. if you're rendering the template from cache, it can be
309
     *                             left null
310
     * @param ICompiler $_compiler the compiler that must be used to compile the template, if left empty a default
311
     *                             Compiler will be used
312
     *
313
     * @return string|void or the template output if $output is false
314
     * @throws Exception
315
     */
316
    public function get($_tpl, $data = array(), $_compiler = null)
317
    {
318
        // a render call came from within a template, so we need a new dwoo instance in order to avoid breaking this one
319
        if ($this->template instanceof ITemplate) {
320
            $clone = clone $this;
321
322
            return $clone->get($_tpl, $data, $_compiler);
323
        }
324
325
        // auto-create template if required
326
        if ($_tpl instanceof ITemplate) {
327
            // valid, skip
328
        } elseif (is_string($_tpl)) {
329
            $_tpl = new TemplateFile($_tpl);
330
            $_tpl->setIncludePath($this->getTemplateDir());
331
        } else {
332
            throw new Exception('Dwoo->get\'s first argument must be a ITemplate (i.e. TemplateFile) or a valid path to a template file', E_USER_NOTICE);
333
        }
334
335
        // save the current template, enters render mode at the same time
336
        // if another rendering is requested it will be proxied to a new Core(instance
337
        $this->template = $_tpl;
338
339
        // load data
340
        if ($data instanceof IDataProvider) {
341
            $this->data = $data->getData();
342
        } elseif (is_array($data)) {
343
            $this->data = $data;
344
        } elseif ($data instanceof ArrayAccess) {
345
            $this->data = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type object<ArrayAccess> is incompatible with the declared type array of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
346
        } else {
347
            throw new Exception('Dwoo->get/Dwoo->output\'s data argument must be a IDataProvider object (i.e. Data) or an associative array', E_USER_NOTICE);
348
        }
349
350
        $this->addGlobal('template', $_tpl->getName());
351
        $this->initRuntimeVars($_tpl);
352
353
        // try to get cached template
354
        $file        = $_tpl->getCachedTemplate($this);
355
        $doCache     = $file === true;
356
        $cacheLoaded = is_string($file);
357
358
        if ($cacheLoaded === true) {
359
            // cache is present, run it
360
            ob_start();
361
            include $file;
362
            $this->template = null;
363
364
            return ob_get_clean();
365
        } else {
366
            $dynamicId = uniqid();
367
368
            // render template
369
            $compiledTemplate = $_tpl->getCompiledTemplate($this, $_compiler);
370
            $out              = include $compiledTemplate;
371
372
            // template returned false so it needs to be recompiled
373
            if ($out === false) {
374
                $_tpl->forceCompilation();
375
                $compiledTemplate = $_tpl->getCompiledTemplate($this, $_compiler);
376
                $out              = include $compiledTemplate;
377
            }
378
379
            if ($doCache === true) {
380
                $out = preg_replace('/(<%|%>|<\?php|<\?|\?>)/', '<?php /*' . $dynamicId . '*/ echo \'$1\'; ?>', $out);
381
                if (!class_exists(self::NAMESPACE_PLUGINS_BLOCKS . 'PluginDynamic')) {
382
                    $this->getLoader()->loadPlugin('PluginDynamic');
383
                }
384
                $out = PluginDynamic::unescape($out, $dynamicId, $compiledTemplate);
385
            }
386
387
            // process filters
388
            foreach ($this->filters as $filter) {
389
                if (is_array($filter) && $filter[0] instanceof Filter) {
390
                    $out = call_user_func($filter, $out);
391
                } else {
392
                    $out = call_user_func($filter, $this, $out);
393
                }
394
            }
395
396
            if ($doCache === true) {
397
                // building cache
398
                $file = $_tpl->cache($this, $out);
399
400
                // run it from the cache to be sure dynamics are rendered
401
                ob_start();
402
                include $file;
403
                // exit render mode
404
                $this->template = null;
405
406
                return ob_get_clean();
407
            } else {
408
                // no need to build cache
409
                // exit render mode
410
                $this->template = null;
411
412
                return $out;
413
            }
414
        }
415
    }
416
417
    /**
418
     * Registers a Global.
419
     * New globals can be added before compiling or rendering a template
420
     * but after, you can only update existing globals.
421
     *
422
     * @param string $name
423
     * @param mixed  $value
424
     *
425
     * @return $this
426
     * @throws Exception
427
     */
428
    public function addGlobal($name, $value)
429
    {
430
        if (null === $this->globals) {
431
            $this->initGlobals();
432
        }
433
434
        $this->globals[$name] = $value;
435
436
        return $this;
437
    }
438
439
    /**
440
     * Gets the registered Globals.
441
     *
442
     * @return array
443
     */
444
    public function getGlobals()
445
    {
446
        return $this->globals;
447
    }
448
449
    /**
450
     * Re-initializes the globals array before each template run.
451
     * this method is only callede once when the Dwoo object is created
452
     *
453
     * @return void
454
     */
455
    protected function initGlobals()
456
    {
457
        $this->globals = array(
458
            'version' => self::VERSION,
459
            'ad'      => '<a href="http://dwoo.org/">Powered by Dwoo</a>',
460
            'now'     => $_SERVER['REQUEST_TIME'],
461
            'charset' => $this->getCharset(),
462
        );
463
    }
464
465
    /**
466
     * Re-initializes the runtime variables before each template run.
467
     * override this method to inject data in the globals array if needed, this
468
     * method is called before each template execution
469
     *
470
     * @param ITemplate $tpl the template that is going to be rendered
471
     *
472
     * @return void
473
     */
474
    protected function initRuntimeVars(ITemplate $tpl)
475
    {
476
        $this->runtimePlugins = array();
477
        $this->scope          = &$this->data;
478
        $this->scopeTree      = array();
479
        $this->stack          = array();
480
        $this->curBlock       = null;
481
        $this->buffer         = '';
482
        $this->returnData     = array();
483
    }
484
485
    /**
486
     * Adds a custom plugin that is not in one of the plugin directories.
487
     *
488
     * @param string   $name       the plugin name to be used in the templates
489
     * @param callback $callback   the plugin callback, either a function name,
490
     *                             a class name or an array containing an object
491
     *                             or class name and a method name
492
     * @param bool     $compilable if set to true, the plugin is assumed to be compilable
493
     *
494
     * @return void
495
     * @throws Exception
496
     */
497
    public function addPlugin($name, $callback, $compilable = false)
498
    {
499
        $compilable = $compilable ? self::COMPILABLE_PLUGIN : 0;
500
        if (is_array($callback)) {
501
            if (is_subclass_of(is_object($callback[0]) ? get_class($callback[0]) : $callback[0], 'Dwoo\Block\Plugin')) {
502
                $this->plugins[$name] = array(
503
                    'type'     => self::BLOCK_PLUGIN | $compilable,
504
                    'callback' => $callback,
505
                    'class'    => (is_object($callback[0]) ? get_class($callback[0]) : $callback[0])
506
                );
507
            } else {
508
                $this->plugins[$name] = array(
509
                    'type'     => self::CLASS_PLUGIN | $compilable,
510
                    'callback' => $callback,
511
                    'class'    => (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]),
512
                    'function' => $callback[1]
513
                );
514
            }
515
        } elseif (is_string($callback)) {
516
            if (class_exists($callback)) {
517 View Code Duplication
                if (is_subclass_of($callback, 'Dwoo\Block\Plugin')) {
518
                    $this->plugins[$name] = array(
519
                        'type'     => self::BLOCK_PLUGIN | $compilable,
520
                        'callback' => $callback,
521
                        'class'    => $callback
522
                    );
523
                } else {
524
                    $this->plugins[$name] = array(
525
                        'type'     => self::CLASS_PLUGIN | $compilable,
526
                        'callback' => $callback,
527
                        'class'    => $callback,
528
                        'function' => ($compilable ? 'compile' : 'process')
529
                    );
530
                }
531
            } elseif (function_exists($callback)) {
532
                $this->plugins[$name] = array(
533
                    'type'     => self::FUNC_PLUGIN | $compilable,
534
                    'callback' => $callback
535
                );
536
            } else {
537
                throw new Exception('Callback could not be processed correctly, please check that the function/class you used exists');
538
            }
539
        } elseif ($callback instanceof Closure) {
540
            $this->plugins[$name] = array(
541
                'type'     => self::FUNC_PLUGIN | $compilable,
542
                'callback' => $callback
543
            );
544
        } elseif (is_object($callback)) {
545 View Code Duplication
            if (is_subclass_of($callback, 'Dwoo\Block\Plugin')) {
546
                $this->plugins[$name] = array(
547
                    'type'     => self::BLOCK_PLUGIN | $compilable,
548
                    'callback' => get_class($callback),
549
                    'class'    => $callback
550
                );
551
            } else {
552
                $this->plugins[$name] = array(
553
                    'type'     => self::CLASS_PLUGIN | $compilable,
554
                    'callback' => $callback,
555
                    'class'    => $callback,
556
                    'function' => ($compilable ? 'compile' : 'process')
557
                );
558
            }
559
        } else {
560
            throw new Exception('Callback could not be processed correctly, please check that the function/class you used exists');
561
        }
562
    }
563
564
    /**
565
     * Removes a custom plugin.
566
     *
567
     * @param string $name the plugin name
568
     *
569
     * @return void
570
     */
571
    public function removePlugin($name)
572
    {
573
        if (isset($this->plugins[$name])) {
574
            unset($this->plugins[$name]);
575
        }
576
    }
577
578
    /**
579
     * Adds a filter to this Dwoo instance, it will be used to filter the output of all the templates rendered by this
580
     * instance.
581
     *
582
     * @param mixed $callback a callback or a filter name if it is autoloaded from a plugin directory
583
     * @param bool  $autoload if true, the first parameter must be a filter name from one of the plugin directories
584
     *
585
     * @return void
586
     * @throws Exception
587
     */
588
    public function addFilter($callback, $autoload = false)
589
    {
590
        if ($autoload) {
591
            $class = self::NAMESPACE_PLUGINS_FILTERS . self::toCamelCase($callback);
592
            if (!class_exists($class) && !function_exists($class)) {
593
                try {
594
                    $this->getLoader()->loadPlugin($callback);
595
                }
596
                catch (Exception $e) {
597
                    if (strstr($callback, self::NAMESPACE_PLUGINS_FILTERS)) {
598
                        throw new Exception('Wrong filter name : ' . $callback . ', the "Filter" prefix should not be used, please only use "' . str_replace('Filter', '', $callback) . '"');
599
                    } else {
600
                        throw new Exception('Wrong filter name : ' . $callback . ', when using autoload the filter must be in one of your plugin dir as "name.php" containig a class or function named "Filter<name>"');
601
                    }
602
                }
603
            }
604
605
            if (class_exists($class)) {
606
                $callback = array(new $class($this), 'process');
607
            } elseif (function_exists($class)) {
608
                $callback = $class;
609
            } else {
610
                throw new Exception('Wrong filter name : ' . $callback . ', when using autoload the filter must be in one of your plugin dir as "name.php" containig a class or function named "Filter<name>"');
611
            }
612
613
            $this->filters[] = $callback;
614
        } else {
615
            $this->filters[] = $callback;
616
        }
617
    }
618
619
    /**
620
     * Removes a filter.
621
     *
622
     * @param mixed $callback callback or filter name if it was autoloaded
623
     *
624
     * @return void
625
     */
626
    public function removeFilter($callback)
627
    {
628
        if (($index = array_search(self::NAMESPACE_PLUGINS_FILTERS. 'Filter' . self::toCamelCase($callback), $this->filters,
629
                true)) !==
630
            false) {
631
            unset($this->filters[$index]);
632
        } elseif (($index = array_search($callback, $this->filters, true)) !== false) {
633
            unset($this->filters[$index]);
634
        } else {
635
            $class = self::NAMESPACE_PLUGINS_FILTERS . 'Filter' . $callback;
636
            foreach ($this->filters as $index => $filter) {
637
                if (is_array($filter) && $filter[0] instanceof $class) {
638
                    unset($this->filters[$index]);
639
                    break;
640
                }
641
            }
642
        }
643
    }
644
645
    /**
646
     * Adds a resource or overrides a default one.
647
     *
648
     * @param string   $name            the resource name
649
     * @param string   $class           the resource class (which must implement ITemplate)
650
     * @param callback $compilerFactory the compiler factory callback, a function that must return a compiler instance
651
     *                                  used to compile this resource, if none is provided. by default it will produce
652
     *                                  a Compiler object
653
     *
654
     * @return void
655
     * @throws Exception
656
     */
657
    public function addResource($name, $class, $compilerFactory = null)
658
    {
659
        if (strlen($name) < 2) {
660
            throw new Exception('Resource names must be at least two-character long to avoid conflicts with Windows paths');
661
        }
662
663
        if (!class_exists($class)) {
664
            throw new Exception(sprintf('Resource class %s does not exist', $class));
665
        }
666
667
        $interfaces = class_implements($class);
668
        if (in_array('Dwoo\ITemplate', $interfaces) === false) {
669
            throw new Exception('Resource class must implement ITemplate');
670
        }
671
672
        $this->resources[$name] = array(
673
            'class'    => $class,
674
            'compiler' => $compilerFactory
675
        );
676
    }
677
678
    /**
679
     * Removes a custom resource.
680
     *
681
     * @param string $name the resource name
682
     *
683
     * @return void
684
     */
685
    public function removeResource($name)
686
    {
687
        unset($this->resources[$name]);
688
        if ($name === 'file') {
689
            $this->resources['file'] = array(
690
                'class'    => 'Dwoo\Template\File',
691
                'compiler' => null
692
            );
693
        }
694
    }
695
696
    /**
697
     * Sets the loader object to use to load plugins.
698
     *
699
     * @param ILoader $loader loader
700
     *
701
     * @return void
702
     */
703
    public function setLoader(ILoader $loader)
704
    {
705
        $this->loader = $loader;
706
    }
707
708
    /**
709
     * Returns the current loader object or a default one if none is currently found.
710
     *
711
     * @return ILoader|Loader
712
     */
713
    public function getLoader()
714
    {
715
        if ($this->loader === null) {
716
            $this->loader = new Loader($this->getCompileDir());
717
        }
718
719
        return $this->loader;
720
    }
721
722
    /**
723
     * Returns the custom plugins loaded.
724
     * Used by the ITemplate classes to pass the custom plugins to their ICompiler instance.
725
     *
726
     * @return array
727
     */
728
    public function getCustomPlugins()
729
    {
730
        return $this->plugins;
731
    }
732
733
    /**
734
     * Return a specified custom plugin loaded by his name.
735
     * Used by the compiler, for executing a Closure.
736
     *
737
     * @param string $name
738
     *
739
     * @return mixed|null
740
     */
741
    public function getCustomPlugin($name)
742
    {
743
        if (isset($this->plugins[$name])) {
744
            return $this->plugins[$name]['callback'];
745
        }
746
747
        return null;
748
    }
749
750
    /**
751
     * Returns the cache directory with a trailing DIRECTORY_SEPARATOR.
752
     *
753
     * @return string
754
     */
755
    public function getCacheDir()
756
    {
757
        if ($this->cacheDir === null) {
758
            $this->setCacheDir(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR);
759
        }
760
761
        return $this->cacheDir;
762
    }
763
764
    /**
765
     * Sets the cache directory and automatically appends a DIRECTORY_SEPARATOR.
766
     *
767
     * @param string $dir the cache directory
768
     *
769
     * @return void
770
     * @throws Exception
771
     */
772 View Code Duplication
    public function setCacheDir($dir)
773
    {
774
        $this->cacheDir = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
775
        if (!file_exists($this->cacheDir)) {
776
            mkdir($this->cacheDir, 0777, true);
777
        }
778
        if (is_writable($this->cacheDir) === false) {
779
            throw new Exception('The cache directory must be writable, chmod "' . $this->cacheDir . '" to make it writable');
780
        }
781
    }
782
783
    /**
784
     * Returns the compile directory with a trailing DIRECTORY_SEPARATOR.
785
     *
786
     * @return string
787
     */
788
    public function getCompileDir()
789
    {
790
        if ($this->compileDir === null) {
791
            $this->setCompileDir(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'compiled' . DIRECTORY_SEPARATOR);
792
        }
793
794
        return $this->compileDir;
795
    }
796
797
    /**
798
     * Sets the compile directory and automatically appends a DIRECTORY_SEPARATOR.
799
     *
800
     * @param string $dir the compile directory
801
     *
802
     * @return void
803
     * @throws Exception
804
     */
805 View Code Duplication
    public function setCompileDir($dir)
806
    {
807
        $this->compileDir = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
808
        if (!file_exists($this->compileDir)) {
809
            mkdir($this->compileDir, 0777, true);
810
        }
811
        if (is_writable($this->compileDir) === false) {
812
            throw new Exception('The compile directory must be writable, chmod "' . $this->compileDir . '" to make it writable');
813
        }
814
    }
815
816
    /**
817
     * Returns an array of the template directory with a trailing DIRECTORY_SEPARATOR
818
     *
819
     * @return array
820
     */
821
    public function getTemplateDir()
822
    {
823
        return $this->templateDir;
824
    }
825
826
    /**
827
     * sets the template directory and automatically appends a DIRECTORY_SEPARATOR
828
     * template directory is stored in an array
829
     *
830
     * @param string $dir
831
     *
832
     * @throws Exception
833
     */
834
    public function setTemplateDir($dir)
835
    {
836
        $tmpDir = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
837
        if (is_dir($tmpDir) === false) {
838
            throw new Exception('The template directory: "' . $tmpDir . '" does not exists, create the directory or specify an other location !');
839
        }
840
        $this->templateDir[] = $tmpDir;
841
    }
842
843
    /**
844
     * Returns the default cache time that is used with templates that do not have a cache time set.
845
     *
846
     * @return int the duration in seconds
847
     */
848
    public function getCacheTime()
849
    {
850
        return $this->cacheTime;
851
    }
852
853
    /**
854
     * Sets the default cache time to use with templates that do not have a cache time set.
855
     *
856
     * @param int $seconds the duration in seconds
857
     *
858
     * @return void
859
     */
860
    public function setCacheTime($seconds)
861
    {
862
        $this->cacheTime = (int)$seconds;
863
    }
864
865
    /**
866
     * Returns the character set used by the string manipulation plugins.
867
     * the charset is automatically lowercased
868
     *
869
     * @return string
870
     */
871
    public function getCharset()
872
    {
873
        return $this->charset;
874
    }
875
876
    /**
877
     * Sets the character set used by the string manipulation plugins.
878
     * the charset will be automatically lowercased
879
     *
880
     * @param string $charset the character set
881
     *
882
     * @return void
883
     */
884
    public function setCharset($charset)
885
    {
886
        $this->charset = strtolower((string)$charset);
887
    }
888
889
    /**
890
     * Returns the current template being rendered, when applicable, or null.
891
     *
892
     * @return ITemplate|null
893
     */
894
    public function getTemplate()
895
    {
896
        return $this->template;
897
    }
898
899
    /**
900
     * Sets the current template being rendered.
901
     *
902
     * @param ITemplate $tpl template object
903
     *
904
     * @return void
905
     */
906
    public function setTemplate(ITemplate $tpl)
907
    {
908
        $this->template = $tpl;
909
    }
910
911
    /**
912
     * Sets the default compiler factory function for the given resource name.
913
     * a compiler factory must return a ICompiler object pre-configured to fit your needs
914
     *
915
     * @param string   $resourceName    the resource name (i.e. file, string)
916
     * @param callback $compilerFactory the compiler factory callback
917
     *
918
     * @return void
919
     */
920
    public function setDefaultCompilerFactory($resourceName, $compilerFactory)
921
    {
922
        $this->resources[$resourceName]['compiler'] = $compilerFactory;
923
    }
924
925
    /**
926
     * Returns the default compiler factory function for the given resource name.
927
     *
928
     * @param string $resourceName the resource name
929
     *
930
     * @return callback the compiler factory callback
931
     */
932
    public function getDefaultCompilerFactory($resourceName)
933
    {
934
        return $this->resources[$resourceName]['compiler'];
935
    }
936
937
    /**
938
     * Sets the security policy object to enforce some php security settings.
939
     * use this if untrusted persons can modify templates
940
     *
941
     * @param SecurityPolicy $policy the security policy object
942
     *
943
     * @return void
944
     */
945
    public function setSecurityPolicy(SecurityPolicy $policy = null)
946
    {
947
        $this->securityPolicy = $policy;
948
    }
949
950
    /**
951
     * Returns the current security policy object or null by default.
952
     *
953
     * @return SecurityPolicy|null the security policy object if any
954
     */
955
    public function getSecurityPolicy()
956
    {
957
        return $this->securityPolicy;
958
    }
959
960
    /**
961
     * Sets the object that must be used as a plugin proxy when plugin can't be found
962
     * by dwoo's loader.
963
     *
964
     * @param IPluginProxy $pluginProxy the proxy object
965
     *
966
     * @return void
967
     */
968
    public function setPluginProxy(IPluginProxy $pluginProxy)
969
    {
970
        $this->pluginProxy = $pluginProxy;
971
    }
972
973
    /**
974
     * Returns the current plugin proxy object or null by default.
975
     *
976
     * @return IPluginProxy
977
     */
978
    public function getPluginProxy()
979
    {
980
        return $this->pluginProxy;
981
    }
982
983
    /**
984
     * Checks whether the given template is cached or not.
985
     *
986
     * @param ITemplate $tpl the template object
987
     *
988
     * @return bool
989
     */
990
    public function isCached(ITemplate $tpl)
991
    {
992
        return is_string($tpl->getCachedTemplate($this));
993
    }
994
995
    /**
996
     * Clear templates inside the compiled directory.
997
     *
998
     * @return int
999
     */
1000
    public function clearCompiled()
1001
    {
1002
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->getCompileDir()), \RecursiveIteratorIterator::SELF_FIRST);
1003
        $count    = 0;
1004
        foreach ($iterator as $file) {
1005
            if ($file->isFile()) {
1006
                $count += unlink($file->__toString()) ? 1 : 0;
1007
            }
1008
        }
1009
1010
        return $count;
1011
    }
1012
1013
    /**
1014
     * Clears the cached templates if they are older than the given time.
1015
     *
1016
     * @param int $olderThan minimum time (in seconds) required for a cached template to be cleared
1017
     *
1018
     * @return int the amount of templates cleared
1019
     */
1020
    public function clearCache($olderThan = - 1)
1021
    {
1022
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->getCacheDir()), \RecursiveIteratorIterator::SELF_FIRST);
1023
        $expired  = time() - $olderThan;
1024
        $count    = 0;
1025
        foreach ($iterator as $file) {
1026
            if ($file->isFile() && $file->getCTime() < $expired) {
1027
                $count += unlink((string)$file) ? 1 : 0;
1028
            }
1029
        }
1030
1031
        return $count;
1032
    }
1033
1034
    /**
1035
     * Fetches a template object of the given resource.
1036
     *
1037
     * @param string    $resourceName   the resource name (i.e. file, string)
1038
     * @param string    $resourceId     the resource identifier (i.e. file path)
1039
     * @param int       $cacheTime      the cache time setting for this resource
1040
     * @param string    $cacheId        the unique cache identifier
1041
     * @param string    $compileId      the unique compiler identifier
1042
     * @param ITemplate $parentTemplate the parent template
1043
     *
1044
     * @return ITemplate
1045
     * @throws Exception
1046
     */
1047
    public function templateFactory($resourceName, $resourceId, $cacheTime = null, $cacheId = null, $compileId = null, ITemplate $parentTemplate = null)
1048
    {
1049
        if (isset($this->resources[$resourceName])) {
1050
            /**
1051
             * Interface ITemplate
1052
             *
1053
             * @var ITemplate $class
1054
             */
1055
            $class = $this->resources[$resourceName]['class'];
1056
1057
            return $class::templateFactory($this, $resourceId, $cacheTime, $cacheId, $compileId, $parentTemplate);
1058
        }
1059
1060
        throw new Exception('Unknown resource type : ' . $resourceName);
1061
    }
1062
1063
    /**
1064
     * Checks if the input is an array or arrayaccess object, optionally it can also check if it's
1065
     * empty.
1066
     *
1067
     * @param mixed $value        the variable to check
1068
     * @param bool  $checkIsEmpty if true, the function will also check if the array|arrayaccess is empty,
1069
     *                            and return true only if it's not empty
1070
     *
1071
     * @return int|bool true if it's an array|arrayaccess (or the item count if $checkIsEmpty is true) or false if it's
1072
     *                  not an array|arrayaccess (or 0 if $checkIsEmpty is true)
1073
     */
1074
    public function isArray($value, $checkIsEmpty = false)
1075
    {
1076
        if (is_array($value) === true || $value instanceof ArrayAccess) {
1077
            if ($checkIsEmpty === false) {
1078
                return true;
1079
            }
1080
1081
            return $this->count($value);
1082
        }
1083
1084
        return false;
1085
    }
1086
1087
    /**
1088
     * Checks if the input is an array or a traversable object, optionally it can also check if it's
1089
     * empty.
1090
     *
1091
     * @param mixed $value        the variable to check
1092
     * @param bool  $checkIsEmpty if true, the function will also check if the array|traversable is empty,
1093
     *                            and return true only if it's not empty
1094
     *
1095
     * @return int|bool true if it's an array|traversable (or the item count if $checkIsEmpty is true) or false if it's
1096
     *                  not an array|traversable (or 0 if $checkIsEmpty is true)
1097
     */
1098
    public function isTraversable($value, $checkIsEmpty = false)
1099
    {
1100
        if (is_array($value) === true) {
1101
            if ($checkIsEmpty === false) {
1102
                return true;
1103
            } else {
1104
                return count($value) > 0;
1105
            }
1106
        } elseif ($value instanceof Traversable) {
1107
            if ($checkIsEmpty === false) {
1108
                return true;
1109
            } else {
1110
                return $this->count($value);
1111
            }
1112
        }
1113
1114
        return false;
1115
    }
1116
1117
    /**
1118
     * Counts an array or arrayaccess/traversable object.
1119
     *
1120
     * @param mixed $value the value to count
1121
     *
1122
     * @return int|bool the count for arrays and objects that implement countable, true for other objects that don't,
1123
     *                  and 0 for empty elements
1124
     */
1125
    public function count($value)
1126
    {
1127
        if (is_array($value) === true || $value instanceof Countable) {
1128
            return count($value);
1129
        } elseif ($value instanceof ArrayAccess) {
1130
            if ($value->offsetExists(0)) {
1131
                return true;
1132
            }
1133
        } elseif ($value instanceof Iterator) {
1134
            $value->rewind();
1135
            if ($value->valid()) {
1136
                return true;
1137
            }
1138
        } elseif ($value instanceof Traversable) {
1139
            foreach ($value as $dummy) {
1140
                return true;
1141
            }
1142
        }
1143
1144
        return 0;
1145
    }
1146
1147
    /**
1148
     * Triggers a dwoo error.
1149
     *
1150
     * @param string $message the error message
1151
     * @param int    $level   the error level, one of the PHP's E_* constants
1152
     *
1153
     * @return void
1154
     */
1155
    public function triggerError($message, $level = E_USER_NOTICE)
1156
    {
1157
        if (!($tplIdentifier = $this->template->getResourceIdentifier())) {
1158
            $tplIdentifier = $this->template->getResourceName();
1159
        }
1160
        trigger_error('Dwoo error (in ' . $tplIdentifier . ') : ' . $message, $level);
1161
    }
1162
1163
    /**
1164
     * Adds a block to the block stack.
1165
     *
1166
     * @param string $blockName the block name (without `Plugin` prefix)
1167
     * @param array  $args      the arguments to be passed to the block's init() function
1168
     *
1169
     * @return BlockPlugin the newly created block
1170
     */
1171
    public function addStack($blockName, array $args = array())
1172
    {
1173
        if (isset($this->plugins[$blockName])) {
1174
            $class = $this->plugins[$blockName]['class'];
1175 View Code Duplication
        } else {
1176
            $class = current(array_filter([
1177
                'Plugin' . self::toCamelCase($blockName),
1178
                self::NAMESPACE_PLUGINS_BLOCKS . 'Plugin' . self::toCamelCase($blockName)
1179
            ], 'class_exists'));
1180
        }
1181
1182
        if ($this->curBlock !== null) {
1183
            $this->curBlock->buffer(ob_get_contents());
1184
            ob_clean();
1185
        } else {
1186
            $this->buffer .= ob_get_contents();
1187
            ob_clean();
1188
        }
1189
1190
        $block = new $class($this);
1191
1192
        call_user_func_array(array($block, 'init'), $args);
1193
1194
        $this->stack[] = $this->curBlock = $block;
1195
1196
        return $block;
1197
    }
1198
1199
    /**
1200
     * Removes the plugin at the top of the block stack.
1201
     * Calls the block buffer() function, followed by a call to end() and finally a call to process()
1202
     *
1203
     * @return void
1204
     */
1205
    public function delStack()
1206
    {
1207
        $args = func_get_args();
1208
1209
        $this->curBlock->buffer(ob_get_contents());
1210
        ob_clean();
1211
1212
        call_user_func_array(array($this->curBlock, 'end'), $args);
1213
1214
        $tmp = array_pop($this->stack);
1215
1216
        if (count($this->stack) > 0) {
1217
            $this->curBlock = end($this->stack);
1218
            $this->curBlock->buffer($tmp->process());
1219
        } else {
1220
            if ($this->buffer !== '') {
1221
                echo $this->buffer;
1222
                $this->buffer = '';
1223
            }
1224
            $this->curBlock = null;
1225
            echo $tmp->process();
1226
        }
1227
1228
        unset($tmp);
1229
    }
1230
1231
    /**
1232
     * Returns the parent block of the given block.
1233
     *
1234
     * @param BlockPlugin $block the block class plugin
1235
     *
1236
     * @return BlockPlugin|false if the given block isn't in the stack
1237
     */
1238
    public function getParentBlock(BlockPlugin $block)
1239
    {
1240
        $index = array_search($block, $this->stack, true);
1241
        if ($index !== false && $index > 0) {
1242
            return $this->stack[$index - 1];
1243
        }
1244
1245
        return false;
1246
    }
1247
1248
    /**
1249
     * Finds the closest block of the given type, starting at the top of the stack.
1250
     *
1251
     * @param string $type the type of plugin you want to find
1252
     *
1253
     * @return BlockPlugin|false if no plugin of such type is in the stack
1254
     */
1255
    public function findBlock($type)
1256
    {
1257
        if (isset($this->plugins[$type])) {
1258
            $type = $this->plugins[$type]['class'];
1259
        } else {
1260
            $type = self::NAMESPACE_PLUGINS_BLOCKS . 'Plugin_' . str_replace(self::NAMESPACE_PLUGINS_BLOCKS.'Plugin',
1261
                    '', $type);
1262
        }
1263
1264
        $keys = array_keys($this->stack);
1265
        while (($key = array_pop($keys)) !== false) {
1266
            if ($this->stack[$key] instanceof $type) {
1267
                return $this->stack[$key];
1268
            }
1269
        }
1270
1271
        return false;
1272
    }
1273
1274
    /**
1275
     * Returns a Plugin of the given class.
1276
     * this is so a single instance of every class plugin is created at each template run,
1277
     * allowing class plugins to have "per-template-run" static variables
1278
     *
1279
     * @param string $class the class name
1280
     *
1281
     * @return mixed an object of the given class
1282
     */
1283
    public function getObjectPlugin($class)
1284
    {
1285
        if (isset($this->runtimePlugins[$class])) {
1286
            return $this->runtimePlugins[$class];
1287
        }
1288
1289
        return $this->runtimePlugins[$class] = new $class($this);
1290
    }
1291
1292
    /**
1293
     * Calls the process() method of the given class-plugin name.
1294
     *
1295
     * @param string $plugName the class plugin name (without `Plugin` prefix)
1296
     * @param array  $params   an array of parameters to send to the process() method
1297
     *
1298
     * @return string the process() return value
1299
     */
1300
    public function classCall($plugName, array $params = array())
1301
    {
1302
        $class  = self::toCamelCase($plugName);
1303
        $plugin = $this->getObjectPlugin($class);
1304
1305
        return call_user_func_array(array($plugin, 'process'), $params);
1306
    }
1307
1308
    /**
1309
     * Calls a php function.
1310
     *
1311
     * @param string $callback the function to call
1312
     * @param array  $params   an array of parameters to send to the function
1313
     *
1314
     * @return mixed the return value of the called function
1315
     */
1316
    public function arrayMap($callback, array $params)
1317
    {
1318
        if ($params[0] === $this) {
1319
            $addThis = true;
1320
            array_shift($params);
1321
        }
1322
        if ((is_array($params[0]) || ($params[0] instanceof Iterator && $params[0] instanceof ArrayAccess))) {
1323
            if (empty($params[0])) {
1324
                return $params[0];
1325
            }
1326
1327
            // array map
1328
            $out = array();
1329
            $cnt = count($params);
1330
1331
            if (isset($addThis)) {
1332
                array_unshift($params, $this);
1333
                $items = $params[1];
1334
                $keys  = array_keys($items);
1335
1336
                if (is_string($callback) === false) {
1337
                    while (($i = array_shift($keys)) !== null) {
1338
                        $out[] = call_user_func_array($callback, array(1 => $items[$i]) + $params);
1339
                    }
1340
                } elseif ($cnt === 1) {
1341
                    while (($i = array_shift($keys)) !== null) {
1342
                        $out[] = $callback($this, $items[$i]);
1343
                    }
1344 View Code Duplication
                } elseif ($cnt === 2) {
1345
                    while (($i = array_shift($keys)) !== null) {
1346
                        $out[] = $callback($this, $items[$i], $params[2]);
1347
                    }
1348
                } elseif ($cnt === 3) {
1349
                    while (($i = array_shift($keys)) !== null) {
1350
                        $out[] = $callback($this, $items[$i], $params[2], $params[3]);
1351
                    }
1352
                } else {
1353
                    while (($i = array_shift($keys)) !== null) {
1354
                        $out[] = call_user_func_array($callback, array(1 => $items[$i]) + $params);
1355
                    }
1356
                }
1357
            } else {
1358
                $items = $params[0];
1359
                $keys  = array_keys($items);
1360
1361
                if (is_string($callback) === false) {
1362
                    while (($i = array_shift($keys)) !== null) {
1363
                        $out[] = call_user_func_array($callback, array($items[$i]) + $params);
1364
                    }
1365
                } elseif ($cnt === 1) {
1366
                    while (($i = array_shift($keys)) !== null) {
1367
                        $out[] = $callback($items[$i]);
1368
                    }
1369 View Code Duplication
                } elseif ($cnt === 2) {
1370
                    while (($i = array_shift($keys)) !== null) {
1371
                        $out[] = $callback($items[$i], $params[1]);
1372
                    }
1373
                } elseif ($cnt === 3) {
1374
                    while (($i = array_shift($keys)) !== null) {
1375
                        $out[] = $callback($items[$i], $params[1], $params[2]);
1376
                    }
1377 View Code Duplication
                } elseif ($cnt === 4) {
1378
                    while (($i = array_shift($keys)) !== null) {
1379
                        $out[] = $callback($items[$i], $params[1], $params[2], $params[3]);
1380
                    }
1381
                } else {
1382
                    while (($i = array_shift($keys)) !== null) {
1383
                        $out[] = call_user_func_array($callback, array($items[$i]) + $params);
1384
                    }
1385
                }
1386
            }
1387
1388
            return $out;
1389
        } else {
1390
            return $params[0];
1391
        }
1392
    }
1393
1394
    /**
1395
     * Reads a variable into the given data array.
1396
     *
1397
     * @param string $varstr   the variable string, using dwoo variable syntax (i.e. "var.subvar[subsubvar]->property")
1398
     * @param mixed  $data     the data array or object to read from
1399
     * @param bool   $safeRead if true, the function will check whether the index exists to prevent any notices from
1400
     *                         being output
1401
     *
1402
     * @return mixed
1403
     */
1404
    public function readVarInto($varstr, $data, $safeRead = false)
1405
    {
1406
        if ($data === null) {
1407
            return null;
1408
        }
1409
1410
        if (is_array($varstr) === false) {
1411
            preg_match_all('#(\[|->|\.)?((?:[^.[\]-]|-(?!>))+)\]?#i', $varstr, $m);
1412
        } else {
1413
            $m = $varstr;
1414
        }
1415
        unset($varstr);
1416
1417
        foreach ($m[1] as $k => $sep) {
1418
            if ($sep === '.' || $sep === '[' || $sep === '') {
1419
                // strip enclosing quotes if present
1420
                $m[2][$k] = preg_replace('#^(["\']?)(.*?)\1$#', '$2', $m[2][$k]);
1421
1422 View Code Duplication
                if ((is_array($data) || $data instanceof ArrayAccess) && ($safeRead === false || isset($data[$m[2][$k]]))) {
1423
                    $data = $data[$m[2][$k]];
1424
                } else {
1425
                    return null;
1426
                }
1427
            } else {
1428
                if (is_object($data) && ($safeRead === false || isset($data->{$m[2][$k]}))) {
1429
                    $data = $data->{$m[2][$k]};
1430
                } else {
1431
                    return null;
1432
                }
1433
            }
1434
        }
1435
1436
        return $data;
1437
    }
1438
1439
    /**
1440
     * Reads a variable into the parent scope.
1441
     *
1442
     * @param int    $parentLevels the amount of parent levels to go from the current scope
1443
     * @param string $varstr       the variable string, using dwoo variable syntax (i.e.
1444
     *                             "var.subvar[subsubvar]->property")
1445
     *
1446
     * @return mixed
1447
     */
1448
    public function readParentVar($parentLevels, $varstr = null)
1449
    {
1450
        $tree = $this->scopeTree;
1451
        $cur  = $this->data;
1452
1453
        while ($parentLevels -- !== 0) {
1454
            array_pop($tree);
1455
        }
1456
1457 View Code Duplication
        while (($i = array_shift($tree)) !== null) {
1458
            if (is_object($cur)) {
1459
                $cur = $cur->{$i};
1460
            } else {
1461
                $cur = $cur[$i];
1462
            }
1463
        }
1464
1465
        if ($varstr !== null) {
1466
            return $this->readVarInto($varstr, $cur);
1467
        } else {
1468
            return $cur;
1469
        }
1470
    }
1471
1472
    /**
1473
     * Reads a variable into the current scope.
1474
     *
1475
     * @param string $varstr the variable string, using dwoo variable syntax (i.e. "var.subvar[subsubvar]->property")
1476
     *
1477
     * @return mixed
1478
     */
1479
    public function readVar($varstr)
1480
    {
1481
        if (is_array($varstr) === true) {
1482
            $m = $varstr;
1483
            unset($varstr);
1484
        } else {
1485
            if (strstr($varstr, '.') === false && strstr($varstr, '[') === false && strstr($varstr, '->') === false) {
1486
                if ($varstr === 'dwoo') {
1487
                    return $this->getGlobals();
1488
                } elseif ($varstr === '__' || $varstr === '_root') {
1489
                    return $this->data;
1490
                } elseif ($varstr === '_' || $varstr === '_parent') {
1491
                    $varstr = '.' . $varstr;
1492
                    $tree   = $this->scopeTree;
1493
                    $cur    = $this->data;
1494
                    array_pop($tree);
1495
1496 View Code Duplication
                    while (($i = array_shift($tree)) !== null) {
1497
                        if (is_object($cur)) {
1498
                            $cur = $cur->{$i};
1499
                        } else {
1500
                            $cur = $cur[$i];
1501
                        }
1502
                    }
1503
1504
                    return $cur;
1505
                }
1506
1507
                $cur = $this->scope;
1508
1509
                if (isset($cur[$varstr])) {
1510
                    return $cur[$varstr];
1511
                } else {
1512
                    return null;
1513
                }
1514
            }
1515
1516
            if (substr($varstr, 0, 1) === '.') {
1517
                $varstr = 'dwoo' . $varstr;
1518
            }
1519
1520
            preg_match_all('#(\[|->|\.)?((?:[^.[\]-]|-(?!>))+)\]?#i', $varstr, $m);
1521
        }
1522
1523
        $i = $m[2][0];
1524
        if ($i === 'dwoo') {
1525
            $cur = $this->getGlobals();
1526
            array_shift($m[2]);
1527
            array_shift($m[1]);
1528
            switch ($m[2][0]) {
1529
            case 'get':
1530
                $cur = $_GET;
1531
                break;
1532
            case 'post':
1533
                $cur = $_POST;
1534
                break;
1535
            case 'session':
1536
                $cur = $_SESSION;
1537
                break;
1538
            case 'cookies':
1539
            case 'cookie':
1540
                $cur = $_COOKIE;
1541
                break;
1542
            case 'server':
1543
                $cur = $_SERVER;
1544
                break;
1545
            case 'env':
1546
                $cur = $_ENV;
1547
                break;
1548
            case 'request':
1549
                $cur = $_REQUEST;
1550
                break;
1551
            case 'const':
1552
                array_shift($m[2]);
1553
                if (defined($m[2][0])) {
1554
                    return constant($m[2][0]);
1555
                } else {
1556
                    return null;
1557
                }
1558
            }
1559
            if ($cur !== $this->getGlobals()) {
1560
                array_shift($m[2]);
1561
                array_shift($m[1]);
1562
            }
1563 View Code Duplication
        } elseif ($i === '__' || $i === '_root') {
1564
            $cur = $this->data;
1565
            array_shift($m[2]);
1566
            array_shift($m[1]);
1567
        } elseif ($i === '_' || $i === '_parent') {
1568
            $tree = $this->scopeTree;
1569
            $cur  = $this->data;
1570
1571
            while (true) {
1572
                array_pop($tree);
1573
                array_shift($m[2]);
1574
                array_shift($m[1]);
1575
                if (current($m[2]) === '_' || current($m[2]) === '_parent') {
1576
                    continue;
1577
                }
1578
1579 View Code Duplication
                while (($i = array_shift($tree)) !== null) {
1580
                    if (is_object($cur)) {
1581
                        $cur = $cur->{$i};
1582
                    } else {
1583
                        $cur = $cur[$i];
1584
                    }
1585
                }
1586
                break;
1587
            }
1588
        } else {
1589
            $cur = $this->scope;
1590
        }
1591
1592
        foreach ($m[1] as $k => $sep) {
1593
            if ($sep === '.' || $sep === '[' || $sep === '') {
1594 View Code Duplication
                if ((is_array($cur) || $cur instanceof ArrayAccess) && isset($cur[$m[2][$k]])) {
1595
                    $cur = $cur[$m[2][$k]];
1596
                } else {
1597
                    return null;
1598
                }
1599 View Code Duplication
            } elseif ($sep === '->') {
1600
                if (is_object($cur)) {
1601
                    $cur = $cur->{$m[2][$k]};
1602
                } else {
1603
                    return null;
1604
                }
1605
            } else {
1606
                return null;
1607
            }
1608
        }
1609
1610
        return $cur;
1611
    }
1612
1613
    /**
1614
     * Assign the value to the given variable.
1615
     *
1616
     * @param mixed  $value the value to assign
1617
     * @param string $scope the variable string, using dwoo variable syntax (i.e. "var.subvar[subsubvar]->property")
1618
     *
1619
     * @return bool true if assigned correctly or false if a problem occured while parsing the var string
1620
     */
1621
    public function assignInScope($value, $scope)
1622
    {
1623
        if (!is_string($scope)) {
1624
            $this->triggerError('Assignments must be done into strings, (' . gettype($scope) . ') ' . var_export($scope, true) . ' given', E_USER_ERROR);
1625
        }
1626
        if (strstr($scope, '.') === false && strstr($scope, '->') === false) {
1627
            $this->scope[$scope] = $value;
1628
        } else {
1629
            // TODO handle _root/_parent scopes ?
1630
            preg_match_all('#(\[|->|\.)?([^.[\]-]+)\]?#i', $scope, $m);
1631
1632
            $cur  = &$this->scope;
1633
            $last = array(
1634
                array_pop($m[1]),
1635
                array_pop($m[2])
1636
            );
1637
1638
            foreach ($m[1] as $k => $sep) {
1639
                if ($sep === '.' || $sep === '[' || $sep === '') {
1640
                    if (is_array($cur) === false) {
1641
                        $cur = array();
1642
                    }
1643
                    $cur = &$cur[$m[2][$k]];
1644 View Code Duplication
                } elseif ($sep === '->') {
1645
                    if (is_object($cur) === false) {
1646
                        $cur = new stdClass();
1647
                    }
1648
                    $cur = &$cur->{$m[2][$k]};
1649
                } else {
1650
                    return false;
1651
                }
1652
            }
1653
1654
            if ($last[0] === '.' || $last[0] === '[' || $last[0] === '') {
1655
                if (is_array($cur) === false) {
1656
                    $cur = array();
1657
                }
1658
                $cur[$last[1]] = $value;
1659
            } elseif ($last[0] === '->') {
1660
                if (is_object($cur) === false) {
1661
                    $cur = new stdClass();
1662
                }
1663
                $cur->{$last[1]} = $value;
1664
            } else {
1665
                return false;
1666
            }
1667
        }
1668
    }
1669
1670
    /**
1671
     * Sets the scope to the given scope string or array.
1672
     *
1673
     * @param mixed $scope    a string i.e. "level1.level2" or an array i.e. array("level1", "level2")
1674
     * @param bool  $absolute if true, the scope is set from the top level scope and not from the current scope
1675
     *
1676
     * @return array the current scope tree
1677
     */
1678
    public function setScope($scope, $absolute = false)
1679
    {
1680
        $old = $this->scopeTree;
1681
1682
        if (is_string($scope) === true) {
1683
            $scope = explode('.', $scope);
1684
        }
1685
1686
        if ($absolute === true) {
1687
            $this->scope     = &$this->data;
1688
            $this->scopeTree = array();
1689
        }
1690
1691
        while (($bit = array_shift($scope)) !== null) {
1692
            if ($bit === '_' || $bit === '_parent') {
1693
                array_pop($this->scopeTree);
1694
                $this->scope = &$this->data;
1695
                $cnt         = count($this->scopeTree);
1696 View Code Duplication
                for ($i = 0; $i < $cnt; ++ $i) {
1697
                    $this->scope = &$this->scope[$this->scopeTree[$i]];
1698
                }
1699 View Code Duplication
            } elseif ($bit === '__' || $bit === '_root') {
1700
                $this->scope     = &$this->data;
1701
                $this->scopeTree = array();
1702
            } elseif (isset($this->scope[$bit])) {
1703
                if ($this->scope instanceof ArrayAccess) {
1704
                    $tmp         = $this->scope[$bit];
1705
                    $this->scope = &$tmp;
1706
                } else {
1707
                    $this->scope = &$this->scope[$bit];
1708
                }
1709
                $this->scopeTree[] = $bit;
1710
            } else {
1711
                unset($this->scope);
1712
                $this->scope = null;
1713
            }
1714
        }
1715
1716
        return $old;
1717
    }
1718
1719
    /**
1720
     * Returns the entire data array.
1721
     *
1722
     * @return array
1723
     */
1724
    public function getData()
1725
    {
1726
        return $this->data;
1727
    }
1728
1729
    /**
1730
     * Sets a return value for the currently running template.
1731
     *
1732
     * @param string $name  var name
1733
     * @param mixed  $value var value
1734
     *
1735
     * @return void
1736
     */
1737
    public function setReturnValue($name, $value)
1738
    {
1739
        $this->returnData[$name] = $value;
1740
    }
1741
1742
    /**
1743
     * Retrieves the return values set by the template.
1744
     *
1745
     * @return array
1746
     */
1747
    public function getReturnValues()
1748
    {
1749
        return $this->returnData;
1750
    }
1751
1752
    /**
1753
     * Returns a reference to the current scope.
1754
     *
1755
     * @return mixed
1756
     */
1757
    public function &getScope()
1758
    {
1759
        return $this->scope;
1760
    }
1761
1762
    /**
1763
     * Redirects all calls to unexisting to plugin proxy.
1764
     *
1765
     * @param string $method the method name
1766
     * @param array  $args   array of arguments
1767
     *
1768
     * @return mixed
1769
     * @throws Exception
1770
     */
1771
    public function __call($method, $args)
1772
    {
1773
        $proxy = $this->getPluginProxy();
1774
        if (!$proxy) {
1775
            throw new Exception('Call to undefined method ' . __CLASS__ . '::' . $method . '()');
1776
        }
1777
1778
        return call_user_func_array($proxy->getCallback($method), $args);
1779
    }
1780
1781
    /**
1782
     * Convert plugin name from `auto_escape` to `AutoEscape`.
1783
     * @param string $input
1784
     * @param string $separator
1785
     *
1786
     * @return mixed
1787
     */
1788
    public static function toCamelCase($input, $separator = '_')
1789
    {
1790
        return join(array_map('ucfirst', explode($separator, $input)));
1791
1792
        // TODO >= PHP5.4.32
1793
        //return str_replace($separator, '', ucwords($input, $separator));
1794
    }
1795
}
1796