1<?php
2
3define('DWOO_DIRECTORY', dirname(__FILE__) . DIRECTORY_SEPARATOR);
4
5/**
6 * main dwoo class, allows communication between the compiler, template and data classes
7 *
8 * <pre>
9 * requirements :
10 *  php 5.2.0 or above (might work below, it's a rough estimate)
11 *  SPL and PCRE extensions (for php versions prior to 5.3.0)
12 *  mbstring extension for some string manipulation plugins (especially if you intend to use UTF-8)
13 * recommended :
14 *  hash extension (for Dwoo_Template_String - minor performance boost)
15 *
16 * project created :
17 *  2008-01-05
18 * </pre>
19 *
20 * This software is provided 'as-is', without any express or implied warranty.
21 * In no event will the authors be held liable for any damages arising from the use of this software.
22 *
23 * @author     Jordi Boggiano <j.boggiano@seld.be>
24 * @copyright  Copyright (c) 2008, Jordi Boggiano
25 * @license    http://dwoo.org/LICENSE   Modified BSD License
26 * @link       http://dwoo.org/
27 * @version    1.1.0
28 * @date       2009-07-18
29 * @package    Dwoo
30 */
31class Dwoo
32{
33	/**
34	 * current version number
35	 *
36	 * @var string
37	 */
38	const VERSION = '1.1.0';
39
40	/**
41	 * unique number of this dwoo release
42	 *
43	 * this can be used by templates classes to check whether the compiled template
44	 * has been compiled before this release or not, so that old templates are
45	 * recompiled automatically when Dwoo is updated
46	 */
47	const RELEASE_TAG = 16;
48
49	/**#@+
50	 * constants that represents all plugin types
51	 *
52	 * these are bitwise-operation-safe values to allow multiple types
53	 * on a single plugin
54	 *
55	 * @var int
56	 */
57	const CLASS_PLUGIN = 1;
58	const FUNC_PLUGIN = 2;
59	const NATIVE_PLUGIN = 4;
60	const BLOCK_PLUGIN = 8;
61	const COMPILABLE_PLUGIN = 16;
62	const CUSTOM_PLUGIN = 32;
63	const SMARTY_MODIFIER = 64;
64	const SMARTY_BLOCK = 128;
65	const SMARTY_FUNCTION = 256;
66	const PROXY_PLUGIN = 512;
67	const TEMPLATE_PLUGIN = 1024;
68	/**#@-*/
69
70	/**
71	 * character set of the template, used by string manipulation plugins
72	 *
73	 * it must be lowercase, but setCharset() will take care of that
74	 *
75	 * @see setCharset
76	 * @see getCharset
77	 * @var string
78	 */
79	protected $charset = 'utf-8';
80
81	/**
82	 * global variables that are accessible through $dwoo.* in the templates
83	 *
84	 * default values include:
85	 *
86	 * $dwoo.version - current version number
87	 * $dwoo.ad - a Powered by Dwoo link pointing to dwoo.org
88	 * $dwoo.now - the current time
89	 * $dwoo.template - the current template filename
90	 * $dwoo.charset - the character set used by the template
91	 *
92	 * on top of that, foreach and other plugins can store special values in there,
93	 * see their documentation for more details.
94	 *
95	 * @private
96	 * @var array
97	 */
98	public $globals;
99
100	/**
101	 * directory where the compiled templates are stored
102	 *
103	 * defaults to DWOO_COMPILEDIR (= dwoo_dir/compiled by default)
104	 *
105	 * @var string
106	 */
107	protected $compileDir;
108
109	/**
110	 * directory where the cached templates are stored
111	 *
112	 * defaults to DWOO_CACHEDIR (= dwoo_dir/cache by default)
113	 *
114	 * @var string
115	 */
116	protected $cacheDir;
117
118	/**
119	 * defines how long (in seconds) the cached files must remain valid
120	 *
121	 * can be overriden on a per-template basis
122	 *
123	 * -1 = never delete
124	 * 0 = disabled
125	 * >0 = duration in seconds
126	 *
127	 * @var int
128	 */
129	protected $cacheTime = 0;
130
131	/**
132	 * security policy object
133	 *
134	 * @var Dwoo_Security_Policy
135	 */
136	protected $securityPolicy = null;
137
138	/**
139	 * stores the custom plugins callbacks
140	 *
141	 * @see addPlugin
142	 * @see removePlugin
143	 * @var array
144	 */
145	protected $plugins = array();
146
147	/**
148	 * stores the filter callbacks
149	 *
150	 * @see addFilter
151	 * @see removeFilter
152	 * @var array
153	 */
154	protected $filters = array();
155
156	/**
157	 * stores the resource types and associated
158	 * classes / compiler classes
159	 *
160	 * @var array
161	 */
162	protected $resources = array
163	(
164		'file'		=>	array
165		(
166			'class'		=>	'Dwoo_Template_File',
167			'compiler'	=>	null
168		),
169		'string'	=>	array
170		(
171			'class'		=>	'Dwoo_Template_String',
172			'compiler'	=>	null
173		)
174	);
175
176	/**
177	 * the dwoo loader object used to load plugins by this dwoo instance
178	 *
179	 * @var Dwoo_ILoader
180	 */
181	protected $loader = null;
182
183	/**
184	 * currently rendered template, set to null when not-rendering
185	 *
186	 * @var Dwoo_ITemplate
187	 */
188	protected $template = null;
189
190	/**
191	 * stores the instances of the class plugins during template runtime
192	 *
193	 * @var array
194	 */
195	protected $runtimePlugins;
196
197	/**
198	 * stores the data during template runtime
199	 *
200	 * @var array
201	 */
202	protected $data;
203
204	/**
205	 * stores the current scope during template runtime
206	 *
207	 * this should ideally not be accessed directly from outside template code
208	 *
209	 * @var mixed
210	 * @private
211	 */
212	public $scope;
213
214	/**
215	 * stores the scope tree during template runtime
216	 *
217	 * @var array
218	 */
219	protected $scopeTree;
220
221	/**
222	 * stores the block plugins stack during template runtime
223	 *
224	 * @var array
225	 */
226	protected $stack;
227
228	/**
229	 * stores the current block plugin at the top of the stack during template runtime
230	 *
231	 * @var Dwoo_Block_Plugin
232	 */
233	protected $curBlock;
234
235	/**
236	 * stores the output buffer during template runtime
237	 *
238	 * @var string
239	 */
240	protected $buffer;
241
242	/**
243	 * stores plugin proxy
244	 *
245	 * @var Dwoo_IPluginProxy
246	 */
247	protected $pluginProxy;
248
249 	/**
250	 * constructor, sets the cache and compile dir to the default values if not provided
251	 *
252	 * @param string $compileDir path to the compiled directory, defaults to lib/compiled
253	 * @param string $cacheDir path to the cache directory, defaults to lib/cache
254	 */
255	public function __construct($compileDir = null, $cacheDir = null)
256	{
257		if ($compileDir !== null) {
258			$this->setCompileDir($compileDir);
259		}
260		if ($cacheDir !== null) {
261			$this->setCacheDir($cacheDir);
262		}
263		$this->initGlobals();
264	}
265
266	/**
267	 * resets some runtime variables to allow a cloned object to be used to render sub-templates
268	 */
269	public function __clone()
270	{
271		$this->template = null;
272		unset($this->data);
273	}
274
275	/**
276	 * outputs the template instead of returning it, this is basically a shortcut for get(*, *, *, true)
277	 *
278	 * @see get
279	 * @param mixed $tpl template, can either be a Dwoo_ITemplate object (i.e. Dwoo_Template_File), a valid path to a template, or
280	 * 					 a template as a string it is recommended to provide a Dwoo_ITemplate as it will probably make things faster,
281	 * 					 especially if you render a template multiple times
282	 * @param mixed $data the data to use, can either be a Dwoo_IDataProvider object (i.e. Dwoo_Data) or an associative array. if you're
283	 * 					  rendering the template from cache, it can be left null
284	 * @param Dwoo_ICompiler $compiler the compiler that must be used to compile the template, if left empty a default
285	 * 								  Dwoo_Compiler will be used.
286	 * @return string nothing or the template output if $output is true
287	 */
288	public function output($tpl, $data = array(), Dwoo_ICompiler $compiler = null)
289	{
290		return $this->get($tpl, $data, $compiler, true);
291	}
292
293	/**
294	 * returns the given template rendered using the provided data and optional compiler
295	 *
296	 * @param mixed $tpl template, can either be a Dwoo_ITemplate object (i.e. Dwoo_Template_File), a valid path to a template, or
297	 * 					 a template as a string it is recommended to provide a Dwoo_ITemplate as it will probably make things faster,
298	 * 					 especially if you render a template multiple times
299	 * @param mixed $data the data to use, can either be a Dwoo_IDataProvider object (i.e. Dwoo_Data) or an associative array. if you're
300	 * 					  rendering the template from cache, it can be left null
301	 * @param Dwoo_ICompiler $compiler the compiler that must be used to compile the template, if left empty a default
302	 * 								  Dwoo_Compiler will be used.
303	 * @param bool $output flag that defines whether the function returns the output of the template (false, default) or echoes it directly (true)
304	 * @return string nothing or the template output if $output is true
305	 */
306	public function get($_tpl, $data = array(), $_compiler = null, $_output = false)
307	{
308		// a render call came from within a template, so we need a new dwoo instance in order to avoid breaking this one
309		if ($this->template instanceof Dwoo_ITemplate) {
310			$proxy = clone $this;
311			return $proxy->get($_tpl, $data, $_compiler, $_output);
312		}
313
314		// auto-create template if required
315		if ($_tpl instanceof Dwoo_ITemplate) {
316			// valid, skip
317		} elseif (is_string($_tpl) && file_exists($_tpl)) {
318			$_tpl = new Dwoo_Template_File($_tpl);
319		} else {
320			throw new Dwoo_Exception('Dwoo->get/Dwoo->output\'s first argument must be a Dwoo_ITemplate (i.e. Dwoo_Template_File) or a valid path to a template file', E_USER_NOTICE);
321		}
322
323		// save the current template, enters render mode at the same time
324		// if another rendering is requested it will be proxied to a new Dwoo instance
325		$this->template = $_tpl;
326
327		// load data
328		if ($data instanceof Dwoo_IDataProvider) {
329			$this->data = $data->getData();
330		} elseif (is_array($data)) {
331			$this->data = $data;
332		} else {
333			throw new Dwoo_Exception('Dwoo->get/Dwoo->output\'s data argument must be a Dwoo_IDataProvider object (i.e. Dwoo_Data) or an associative array', E_USER_NOTICE);
334		}
335
336		$this->globals['template'] = $_tpl->getName();
337		$this->initRuntimeVars($_tpl);
338
339		// try to get cached template
340		$file = $_tpl->getCachedTemplate($this);
341		$doCache = $file === true;
342		$cacheLoaded = is_string($file);
343
344		if ($cacheLoaded === true) {
345			// cache is present, run it
346			if ($_output === true) {
347				include $file;
348				$this->template = null;
349			} else {
350				ob_start();
351				include $file;
352				$this->template = null;
353				return ob_get_clean();
354			}
355		} else {
356			// no cache present
357
358			if ($doCache === true) {
359				$dynamicId = uniqid();
360			}
361
362			// render template
363			$out = include $_tpl->getCompiledTemplate($this, $_compiler);
364
365			// template returned false so it needs to be recompiled
366			if ($out === false) {
367				$_tpl->forceCompilation();
368				$out = include $_tpl->getCompiledTemplate($this, $_compiler);
369			}
370
371			if ($doCache === true) {
372				$out = preg_replace('/(<%|%>|<\?php|<\?|\?>)/', '<?php /*'.$dynamicId.'*/ echo \'$1\'; ?>', $out);
373				if (!class_exists('Dwoo_plugin_dynamic', false)) {
374					$this->getLoader()->loadPlugin('dynamic');
375				}
376				$out = Dwoo_Plugin_dynamic::unescape($out, $dynamicId);
377			}
378
379			// process filters
380			foreach ($this->filters as $filter) {
381				if (is_array($filter) && $filter[0] instanceof Dwoo_Filter) {
382					$out = call_user_func($filter, $out);
383				} else {
384					$out = call_user_func($filter, $this, $out);
385				}
386			}
387
388			if ($doCache === true) {
389				// building cache
390				$file = $_tpl->cache($this, $out);
391
392				// run it from the cache to be sure dynamics are rendered
393				if ($_output === true) {
394					include $file;
395					// exit render mode
396					$this->template = null;
397				} else {
398					ob_start();
399					include $file;
400					// exit render mode
401					$this->template = null;
402					return ob_get_clean();
403				}
404			} else {
405				// no need to build cache
406				// exit render mode
407				$this->template = null;
408				// output
409				if ($_output === true) {
410					echo $out;
411				} else {
412					return $out;
413				}
414			}
415		}
416	}
417
418	/**
419	 * re-initializes the globals array before each template run
420	 *
421	 * this method is only callede once when the Dwoo object is created
422	 */
423	protected function initGlobals()
424	{
425		$this->globals = array
426		(
427			'version'	=>	self::VERSION,
428			'ad'		=>	'<a href="http://dwoo.org/">Powered by Dwoo</a>',
429			'now'		=>	$_SERVER['REQUEST_TIME'],
430			'charset'	=>	$this->charset,
431		);
432	}
433
434	/**
435	 * re-initializes the runtime variables before each template run
436	 *
437	 * override this method to inject data in the globals array if needed, this
438	 * method is called before each template execution
439	 *
440	 * @param Dwoo_ITemplate $tpl the template that is going to be rendered
441	 */
442	protected function initRuntimeVars(Dwoo_ITemplate $tpl)
443	{
444		$this->runtimePlugins = array();
445		$this->scope =& $this->data;
446		$this->scopeTree = array();
447		$this->stack = array();
448		$this->curBlock = null;
449		$this->buffer = '';
450	}
451
452	/*
453	 * --------- settings functions ---------
454	 */
455
456	/**
457	 * adds a custom plugin that is not in one of the plugin directories
458	 *
459	 * @param string $name the plugin name to be used in the templates
460	 * @param callback $callback the plugin callback, either a function name,
461	 * 							 a class name or an array containing an object
462	 * 							 or class name and a method name
463	 * @param bool $compilable if set to true, the plugin is assumed to be compilable
464	 */
465	public function addPlugin($name, $callback, $compilable = false)
466	{
467		$compilable = $compilable ? self::COMPILABLE_PLUGIN : 0;
468		if (is_array($callback)) {
469			if (is_subclass_of(is_object($callback[0]) ? get_class($callback[0]) : $callback[0], 'Dwoo_Block_Plugin')) {
470				$this->plugins[$name] = array('type'=>self::BLOCK_PLUGIN | $compilable, 'callback'=>$callback, 'class'=>(is_object($callback[0]) ? get_class($callback[0]) : $callback[0]));
471			} else {
472				$this->plugins[$name] = array('type'=>self::CLASS_PLUGIN | $compilable, 'callback'=>$callback, 'class'=>(is_object($callback[0]) ? get_class($callback[0]) : $callback[0]), 'function'=>$callback[1]);
473			}
474		} elseif (class_exists($callback, false)) {
475			if (is_subclass_of($callback, 'Dwoo_Block_Plugin')) {
476				$this->plugins[$name] = array('type'=>self::BLOCK_PLUGIN | $compilable, 'callback'=>$callback, 'class'=>$callback);
477			} else {
478				$this->plugins[$name] = array('type'=>self::CLASS_PLUGIN | $compilable, 'callback'=>$callback, 'class'=>$callback, 'function'=>'process');
479			}
480		} elseif (function_exists($callback)) {
481			$this->plugins[$name] = array('type'=>self::FUNC_PLUGIN | $compilable, 'callback'=>$callback);
482		} else {
483			throw new Dwoo_Exception('Callback could not be processed correctly, please check that the function/class you used exists');
484		}
485	}
486
487	/**
488	 * removes a custom plugin
489	 *
490	 * @param string $name the plugin name
491	 */
492	public function removePlugin($name)
493	{
494		if (isset($this->plugins[$name])) {
495			unset($this->plugins[$name]);
496		}
497	}
498
499	/**
500	 * adds a filter to this Dwoo instance, it will be used to filter the output of all the templates rendered by this instance
501	 *
502	 * @param mixed $callback a callback or a filter name if it is autoloaded from a plugin directory
503	 * @param bool $autoload if true, the first parameter must be a filter name from one of the plugin directories
504	 */
505	public function addFilter($callback, $autoload = false)
506	{
507		if ($autoload) {
508			$class = 'Dwoo_Filter_'.$callback;
509
510			if (!class_exists($class, false) && !function_exists($class)) {
511				try {
512					$this->getLoader()->loadPlugin($callback);
513				} catch (Dwoo_Exception $e) {
514					if (strstr($callback, 'Dwoo_Filter_')) {
515						throw new Dwoo_Exception('Wrong filter name : '.$callback.', the "Dwoo_Filter_" prefix should not be used, please only use "'.str_replace('Dwoo_Filter_', '', $callback).'"');
516					} else {
517						throw new Dwoo_Exception('Wrong filter name : '.$callback.', when using autoload the filter must be in one of your plugin dir as "name.php" containg a class or function named "Dwoo_Filter_name"');
518					}
519				}
520			}
521
522			if (class_exists($class, false)) {
523				$callback = array(new $class($this), 'process');
524			} elseif (function_exists($class)) {
525				$callback = $class;
526			} else {
527				throw new Dwoo_Exception('Wrong filter name : '.$callback.', when using autoload the filter must be in one of your plugin dir as "name.php" containg a class or function named "Dwoo_Filter_name"');
528			}
529
530			$this->filters[] = $callback;
531		} else {
532			$this->filters[] = $callback;
533		}
534	}
535
536	/**
537	 * removes a filter
538	 *
539	 * @param mixed $callback callback or filter name if it was autoloaded
540	 */
541	public function removeFilter($callback)
542	{
543		if (($index = array_search('Dwoo_Filter_'.$callback, $this->filters, true)) !== false) {
544			unset($this->filters[$index]);
545		} elseif (($index = array_search($callback, $this->filters, true)) !== false) {
546			unset($this->filters[$index]);
547		} else	{
548			$class = 'Dwoo_Filter_' . $callback;
549			foreach ($this->filters as $index=>$filter) {
550				if (is_array($filter) && $filter[0] instanceof $class) {
551					unset($this->filters[$index]);
552					break;
553				}
554			}
555		}
556	}
557
558	/**
559	 * adds a resource or overrides a default one
560	 *
561	 * @param string $name the resource name
562	 * @param string $class the resource class (which must implement Dwoo_ITemplate)
563	 * @param callback $compilerFactory the compiler factory callback, a function that must return a compiler instance used to compile this resource, if none is provided. by default it will produce a Dwoo_Compiler object
564	 */
565	public function addResource($name, $class, $compilerFactory = null)
566	{
567		if (strlen($name) < 2) {
568			throw new Dwoo_Exception('Resource names must be at least two-character long to avoid conflicts with Windows paths');
569		}
570
571		if (!class_exists($class)) {
572			throw new Dwoo_Exception('Resource class does not exist');
573		}
574
575		$interfaces = class_implements($class);
576		if (in_array('Dwoo_ITemplate', $interfaces) === false) {
577			throw new Dwoo_Exception('Resource class must implement Dwoo_ITemplate');
578		}
579
580		$this->resources[$name] = array('class'=>$class, 'compiler'=>$compilerFactory);
581	}
582
583	/**
584	 * removes a custom resource
585	 *
586	 * @param string $name the resource name
587	 */
588	public function removeResource($name)
589	{
590		unset($this->resources[$name]);
591		if ($name==='file') {
592			$this->resources['file'] = array('class'=>'Dwoo_Template_File', 'compiler'=>null);
593		}
594	}
595
596	/*
597	 * --------- getters and setters ---------
598	 */
599
600	/**
601	 * sets the loader object to use to load plugins
602	 *
603	 * @param Dwoo_ILoader $loader loader object
604	 */
605	public function setLoader(Dwoo_ILoader $loader)
606	{
607		$this->loader = $loader;
608	}
609
610	/**
611	 * returns the current loader object or a default one if none is currently found
612	 *
613	 * @param Dwoo_ILoader
614	 */
615	public function getLoader()
616	{
617		if ($this->loader === null) {
618			$this->loader = new Dwoo_Loader($this->getCompileDir());
619		}
620
621		return $this->loader;
622	}
623
624	/**
625	 * returns the custom plugins loaded
626	 *
627	 * used by the Dwoo_ITemplate classes to pass the custom plugins to their Dwoo_ICompiler instance
628	 *
629	 * @return array
630	 */
631	public function getCustomPlugins()
632	{
633		return $this->plugins;
634	}
635
636	/**
637	 * returns the cache directory with a trailing DIRECTORY_SEPARATOR
638	 *
639	 * @return string
640	 */
641	public function getCacheDir()
642	{
643		if ($this->cacheDir === null) {
644			$this->setCacheDir(dirname(__FILE__).DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR);
645		}
646
647		return $this->cacheDir;
648	}
649
650	/**
651	 * sets the cache directory and automatically appends a DIRECTORY_SEPARATOR
652	 *
653	 * @param string $dir the cache directory
654	 */
655	public function setCacheDir($dir)
656	{
657		$this->cacheDir = rtrim($dir, '/\\').DIRECTORY_SEPARATOR;
658		if (is_writable($this->cacheDir) === false) {
659			throw new Dwoo_Exception('The cache directory must be writable, chmod "'.$this->cacheDir.'" to make it writable');
660		}
661	}
662
663	/**
664	 * returns the compile directory with a trailing DIRECTORY_SEPARATOR
665	 *
666	 * @return string
667	 */
668	public function getCompileDir()
669	{
670		if ($this->compileDir === null) {
671			$this->setCompileDir(dirname(__FILE__).DIRECTORY_SEPARATOR.'compiled'.DIRECTORY_SEPARATOR);
672		}
673
674		return $this->compileDir;
675	}
676
677	/**
678	 * sets the compile directory and automatically appends a DIRECTORY_SEPARATOR
679	 *
680	 * @param string $dir the compile directory
681	 */
682	public function setCompileDir($dir)
683	{
684		$this->compileDir = rtrim($dir, '/\\').DIRECTORY_SEPARATOR;
685		if (is_writable($this->compileDir) === false) {
686			throw new Dwoo_Exception('The compile directory must be writable, chmod "'.$this->compileDir.'" to make it writable');
687		}
688	}
689
690	/**
691	 * returns the default cache time that is used with templates that do not have a cache time set
692	 *
693	 * @return int the duration in seconds
694	 */
695	public function getCacheTime()
696	{
697		return $this->cacheTime;
698	}
699
700	/**
701	 * sets the default cache time to use with templates that do not have a cache time set
702	 *
703	 * @param int $seconds the duration in seconds
704	 */
705	public function setCacheTime($seconds)
706	{
707		$this->cacheTime = (int) $seconds;
708	}
709
710	/**
711	 * returns the character set used by the string manipulation plugins
712	 *
713	 * the charset is automatically lowercased
714	 *
715	 * @return string
716	 */
717	public function getCharset()
718	{
719		return $this->charset;
720	}
721
722	/**
723	 * sets the character set used by the string manipulation plugins
724	 *
725	 * the charset will be automatically lowercased
726	 *
727	 * @param string $charset the character set
728	 */
729	public function setCharset($charset)
730	{
731		$this->charset = strtolower((string) $charset);
732	}
733
734	/**
735	 * returns the current template being rendered, when applicable, or null
736	 *
737	 * @return Dwoo_ITemplate|null
738	 */
739	public function getTemplate()
740	{
741		return $this->template;
742	}
743
744	/**
745	 * sets the default compiler factory function for the given resource name
746	 *
747	 * a compiler factory must return a Dwoo_ICompiler object pre-configured to fit your needs
748	 *
749	 * @param string $resourceName the resource name (i.e. file, string)
750	 * @param callback $compilerFactory the compiler factory callback
751	 */
752	public function setDefaultCompilerFactory($resourceName, $compilerFactory)
753	{
754		$this->resources[$resourceName]['compiler'] = $compilerFactory;
755	}
756
757	/**
758	 * returns the default compiler factory function for the given resource name
759	 *
760	 * @param string $resourceName the resource name
761	 * @return callback the compiler factory callback
762	 */
763	public function getDefaultCompilerFactory($resourceName)
764	{
765		return $this->resources[$resourceName]['compiler'];
766	}
767
768	/**
769	 * sets the security policy object to enforce some php security settings
770	 *
771	 * use this if untrusted persons can modify templates
772	 *
773	 * @param Dwoo_Security_Policy $policy the security policy object
774	 */
775	public function setSecurityPolicy(Dwoo_Security_Policy $policy = null)
776	{
777		$this->securityPolicy = $policy;
778	}
779
780	/**
781	 * returns the current security policy object or null by default
782	 *
783	 * @return Dwoo_Security_Policy|null the security policy object if any
784	 */
785	public function getSecurityPolicy()
786	{
787		return $this->securityPolicy;
788	}
789
790	/**
791	 * sets the object that must be used as a plugin proxy when plugin can't be found
792	 * by dwoo's loader
793	 *
794	 * @param Dwoo_IPluginProxy $pluginProxy the proxy object
795	 */
796	public function setPluginProxy(Dwoo_IPluginProxy $pluginProxy) {
797		$this->pluginProxy = $pluginProxy;
798	}
799
800	/**
801	 * returns the current plugin proxy object or null by default
802	 *
803	 * @param Dwoo_IPluginProxy|null the proxy object if any
804	 */
805	public function getPluginProxy() {
806		return $this->pluginProxy;
807	}
808
809	/*
810	 * --------- util functions ---------
811	 */
812
813	/**
814	 * [util function] checks whether the given template is cached or not
815	 *
816	 * @param Dwoo_ITemplate $tpl the template object
817	 * @return bool
818	 */
819	public function isCached(Dwoo_ITemplate $tpl)
820	{
821		return is_string($tpl->getCachedTemplate($this));
822	}
823
824	/**
825	 * [util function] clears the cached templates if they are older than the given time
826	 *
827	 * @param int $olderThan minimum time (in seconds) required for a cached template to be cleared
828	 * @return int the amount of templates cleared
829	 */
830	public function clearCache($olderThan=-1)
831	{
832		$cacheDirs = new RecursiveDirectoryIterator($this->getCacheDir());
833		$cache = new RecursiveIteratorIterator($cacheDirs);
834		$expired = time() - $olderThan;
835		$count = 0;
836		foreach ($cache as $file) {
837			if ($cache->isDot() || $cache->isDir() || substr($file, -5) !== '.html') {
838				continue;
839			}
840			if ($cache->getCTime() < $expired) {
841				$count += unlink((string) $file) ? 1 : 0;
842			}
843		}
844		return $count;
845	}
846
847	/**
848	 * [util function] fetches a template object of the given resource
849	 *
850	 * @param string $resourceName the resource name (i.e. file, string)
851	 * @param string $resourceId the resource identifier (i.e. file path)
852	 * @param int $cacheTime the cache time setting for this resource
853	 * @param string $cacheId the unique cache identifier
854	 * @param string $compileId the unique compiler identifier
855	 * @return Dwoo_ITemplate
856	 */
857	public function templateFactory($resourceName, $resourceId, $cacheTime = null, $cacheId = null, $compileId = null, Dwoo_ITemplate $parentTemplate = null)
858	{
859		if (isset($this->resources[$resourceName])) {
860			// TODO could be changed to $this->resources[$resourceName]['class']::templateFactory(..) in 5.3 maybe
861			return call_user_func(array($this->resources[$resourceName]['class'], 'templateFactory'), $this, $resourceId, $cacheTime, $cacheId, $compileId, $parentTemplate);
862		} else {
863			throw new Dwoo_Exception('Unknown resource type : '.$resourceName);
864		}
865	}
866
867	/**
868	 * [util function] checks if the input is an array or an iterator object, optionally it can also check if it's empty
869	 *
870	 * @param mixed $value the variable to check
871	 * @param bool $checkIsEmpty if true, the function will also check if the array is empty,
872	 * 								and return true only if it's not empty
873	 * @return bool true if it's an array (and not empty) or false if it's not an array (or if it's empty)
874	 */
875	public function isArray($value, $checkIsEmpty=false)
876	{
877		if (is_array($value) === true) {
878			if ($checkIsEmpty === false) {
879				return true;
880			} else {
881				return count($value) > 0;
882			}
883		} elseif ($value instanceof Iterator) {
884			if ($checkIsEmpty === false) {
885				return true;
886			} elseif ($value instanceof Countable) {
887				return count($value) > 0;
888			} else {
889				$value->rewind();
890				return $value->valid();
891			}
892		} elseif ($value instanceof ArrayAccess) {
893			if ($checkIsEmpty === false) {
894				return true;
895			} elseif ($value instanceof Countable) {
896				return count($value) > 0;
897			} else {
898				return $value->offsetExists(0);
899			}
900		}
901		return false;
902	}
903
904	/**
905	 * [util function] triggers a dwoo error
906	 *
907	 * @param string $message the error message
908	 * @param int $level the error level, one of the PHP's E_* constants
909	 */
910	public function triggerError($message, $level=E_USER_NOTICE)
911	{
912		if (!($tplIdentifier = $this->template->getResourceIdentifier())) {
913			$tplIdentifier = $this->template->getResourceName();
914		}
915		trigger_error('Dwoo error (in '.$tplIdentifier.') : '.$message, $level);
916	}
917
918	/*
919	 * --------- runtime functions ---------
920	 */
921
922	/**
923	 * [runtime function] adds a block to the block stack
924	 *
925	 * @param string $blockName the block name (without Dwoo_Plugin_ prefix)
926	 * @param array $args the arguments to be passed to the block's init() function
927	 * @return Dwoo_Block_Plugin the newly created block
928	 */
929	public function addStack($blockName, array $args=array())
930	{
931		if (isset($this->plugins[$blockName])) {
932			$class = $this->plugins[$blockName]['class'];
933		} else {
934			$class = 'Dwoo_Plugin_'.$blockName;
935		}
936
937		if ($this->curBlock !== null) {
938			$this->curBlock->buffer(ob_get_contents());
939			ob_clean();
940		} else {
941			$this->buffer .= ob_get_contents();
942			ob_clean();
943		}
944
945		$block = new $class($this);
946
947		$cnt = count($args);
948		if ($cnt===0) {
949			$block->init();
950		} elseif ($cnt===1) {
951			$block->init($args[0]);
952		} elseif ($cnt===2) {
953			$block->init($args[0], $args[1]);
954		} elseif ($cnt===3) {
955			$block->init($args[0], $args[1], $args[2]);
956		} elseif ($cnt===4) {
957			$block->init($args[0], $args[1], $args[2], $args[3]);
958		} else {
959			call_user_func_array(array($block,'init'), $args);
960		}
961
962		$this->stack[] = $this->curBlock = $block;
963		return $block;
964	}
965
966	/**
967	 * [runtime function] removes the plugin at the top of the block stack
968	 *
969	 * calls the block buffer() function, followed by a call to end()
970	 * and finally a call to process()
971	 */
972	public function delStack()
973	{
974		$args = func_get_args();
975
976		$this->curBlock->buffer(ob_get_contents());
977		ob_clean();
978
979		$cnt = count($args);
980		if ($cnt===0) {
981			$this->curBlock->end();
982		} elseif ($cnt===1) {
983			$this->curBlock->end($args[0]);
984		} elseif ($cnt===2) {
985			$this->curBlock->end($args[0], $args[1]);
986		} elseif ($cnt===3) {
987			$this->curBlock->end($args[0], $args[1], $args[2]);
988		} elseif ($cnt===4) {
989			$this->curBlock->end($args[0], $args[1], $args[2], $args[3]);
990		} else {
991			call_user_func_array(array($this->curBlock, 'end'), $args);
992		}
993
994		$tmp = array_pop($this->stack);
995
996		if (count($this->stack) > 0) {
997			$this->curBlock = end($this->stack);
998			$this->curBlock->buffer($tmp->process());
999		} else {
1000			$this->curBlock = null;
1001			echo $tmp->process();
1002		}
1003
1004		unset($tmp);
1005	}
1006
1007	/**
1008	 * [runtime function] returns the parent block of the given block
1009	 *
1010	 * @param Dwoo_Block_Plugin $block
1011	 * @return Dwoo_Block_Plugin or false if the given block isn't in the stack
1012	 */
1013	public function getParentBlock(Dwoo_Block_Plugin $block)
1014	{
1015		$index = array_search($block, $this->stack, true);
1016		if ($index !== false && $index > 0) {
1017			return $this->stack[$index-1];
1018		}
1019		return false;
1020	}
1021
1022	/**
1023	 * [runtime function] finds the closest block of the given type, starting at the top of the stack
1024	 *
1025	 * @param string $type the type of plugin you want to find
1026	 * @return Dwoo_Block_Plugin or false if no plugin of such type is in the stack
1027	 */
1028	public function findBlock($type)
1029	{
1030		if (isset($this->plugins[$type])) {
1031			$type = $this->plugins[$type]['class'];
1032		} else {
1033			$type = 'Dwoo_Plugin_'.str_replace('Dwoo_Plugin_', '', $type);
1034		}
1035
1036		$keys = array_keys($this->stack);
1037		while (($key = array_pop($keys)) !== false) {
1038			if ($this->stack[$key] instanceof $type) {
1039				return $this->stack[$key];
1040			}
1041		}
1042		return false;
1043	}
1044
1045	/**
1046	 * [runtime function] returns a Dwoo_Plugin of the given class
1047	 *
1048	 * this is so a single instance of every class plugin is created at each template run,
1049	 * allowing class plugins to have "per-template-run" static variables
1050	 *
1051	 * @param string $class the class name
1052	 * @return mixed an object of the given class
1053	 */
1054	protected function getObjectPlugin($class)
1055	{
1056		if (isset($this->runtimePlugins[$class])) {
1057			return $this->runtimePlugins[$class];
1058		}
1059		return $this->runtimePlugins[$class] = new $class($this);
1060	}
1061
1062	/**
1063	 * [runtime function] calls the process() method of the given class-plugin name
1064	 *
1065	 * @param string $plugName the class plugin name (without Dwoo_Plugin_ prefix)
1066	 * @param array $params an array of parameters to send to the process() method
1067	 * @return string the process() return value
1068	 */
1069	public function classCall($plugName, array $params = array())
1070	{
1071		$class = 'Dwoo_Plugin_'.$plugName;
1072
1073		$plugin = $this->getObjectPlugin($class);
1074
1075		$cnt = count($params);
1076		if ($cnt===0) {
1077			return $plugin->process();
1078		} elseif ($cnt===1) {
1079			return $plugin->process($params[0]);
1080		} elseif ($cnt===2) {
1081			return $plugin->process($params[0], $params[1]);
1082		} elseif ($cnt===3) {
1083			return $plugin->process($params[0], $params[1], $params[2]);
1084		} elseif ($cnt===4) {
1085			return $plugin->process($params[0], $params[1], $params[2], $params[3]);
1086		} else {
1087			return call_user_func_array(array($plugin, 'process'), $params);
1088		}
1089	}
1090
1091	/**
1092	 * [runtime function] calls a php function
1093	 *
1094	 * @param string $callback the function to call
1095	 * @param array $params an array of parameters to send to the function
1096	 * @return mixed the return value of the called function
1097	 */
1098	public function arrayMap($callback, array $params)
1099	{
1100		if ($params[0] === $this) {
1101			$addThis = true;
1102			array_shift($params);
1103		}
1104		if ((is_array($params[0]) || ($params[0] instanceof Iterator && $params[0] instanceof ArrayAccess))) {
1105			if (empty($params[0])) {
1106				return $params[0];
1107			}
1108
1109			// array map
1110			$out = array();
1111			$cnt = count($params);
1112
1113			if (isset($addThis)) {
1114				array_unshift($params, $this);
1115				$items = $params[1];
1116				$keys = array_keys($items);
1117
1118				if (is_string($callback) === false) {
1119					while (($i = array_shift($keys)) !== null) {
1120						$out[] = call_user_func_array($callback, array(1=>$items[$i]) + $params);
1121					}
1122				} elseif ($cnt===1) {
1123					while (($i = array_shift($keys)) !== null) {
1124						$out[] = $callback($this, $items[$i]);
1125					}
1126				} elseif ($cnt===2) {
1127					while (($i = array_shift($keys)) !== null) {
1128						$out[] = $callback($this, $items[$i], $params[2]);
1129					}
1130				} elseif ($cnt===3) {
1131					while (($i = array_shift($keys)) !== null) {
1132						$out[] = $callback($this, $items[$i], $params[2], $params[3]);
1133					}
1134				} else {
1135					while (($i = array_shift($keys)) !== null) {
1136						$out[] = call_user_func_array($callback, array(1=>$items[$i]) + $params);
1137					}
1138				}
1139			} else {
1140				$items = $params[0];
1141				$keys = array_keys($items);
1142
1143				if (is_string($callback) === false) {
1144					while (($i = array_shift($keys)) !== null) {
1145						$out[] = call_user_func_array($callback, array($items[$i]) + $params);
1146					}
1147				} elseif ($cnt===1) {
1148					while (($i = array_shift($keys)) !== null) {
1149						$out[] = $callback($items[$i]);
1150					}
1151				} elseif ($cnt===2) {
1152					while (($i = array_shift($keys)) !== null) {
1153						$out[] = $callback($items[$i], $params[1]);
1154					}
1155				} elseif ($cnt===3) {
1156					while (($i = array_shift($keys)) !== null) {
1157						$out[] = $callback($items[$i], $params[1], $params[2]);
1158					}
1159				} elseif ($cnt===4) {
1160					while (($i = array_shift($keys)) !== null) {
1161						$out[] = $callback($items[$i], $params[1], $params[2], $params[3]);
1162					}
1163				} else {
1164					while (($i = array_shift($keys)) !== null) {
1165						$out[] = call_user_func_array($callback, array($items[$i]) + $params);
1166					}
1167				}
1168			}
1169			return $out;
1170		} else {
1171			return $params[0];
1172		}
1173	}
1174
1175	/**
1176	 * [runtime function] reads a variable into the given data array
1177	 *
1178	 * @param string $varstr the variable string, using dwoo variable syntax (i.e. "var.subvar[subsubvar]->property")
1179	 * @param mixed $data the data array or object to read from
1180	 * @param bool $safeRead if true, the function will check whether the index exists to prevent any notices from being output
1181	 * @return mixed
1182	 */
1183	public function readVarInto($varstr, $data, $safeRead = false)
1184	{
1185		if ($data === null) {
1186			return null;
1187		}
1188
1189		if (is_array($varstr) === false) {
1190			preg_match_all('#(\[|->|\.)?([^.[\]-]+)\]?#i', $varstr, $m);
1191		} else {
1192			$m = $varstr;
1193		}
1194		unset($varstr);
1195
1196                foreach ($m[1] as $k => $sep) {
1197			if ($sep === '.' || $sep === '[' || $sep === '') {
1198				if ((is_array($data) || $data instanceof ArrayAccess) && ($safeRead === false || isset($data[$m[2][$k]]))) {
1199					$data = $data[$m[2][$k]];
1200				} else {
1201					return null;
1202				}
1203			} else {
1204				if (is_object($data) && ($safeRead === false || isset($data->$m[2][$k]))) {
1205					$data = $data->$m[2][$k];
1206				} else {
1207					return null;
1208				}
1209			}
1210		}
1211
1212		return $data;
1213	}
1214
1215	/**
1216	 * [runtime function] reads a variable into the parent scope
1217	 *
1218	 * @param int $parentLevels the amount of parent levels to go from the current scope
1219	 * @param string $varstr the variable string, using dwoo variable syntax (i.e. "var.subvar[subsubvar]->property")
1220	 * @return mixed
1221	 */
1222	public function readParentVar($parentLevels, $varstr = null)
1223	{
1224		$tree = $this->scopeTree;
1225		$cur = $this->data;
1226
1227		while ($parentLevels--!==0) {
1228			array_pop($tree);
1229		}
1230
1231		while (($i = array_shift($tree)) !== null) {
1232			if (is_object($cur)) {
1233				$cur = $cur->$i;
1234			} else {
1235				$cur = $cur[$i];
1236			}
1237		}
1238
1239		if ($varstr!==null) {
1240			return $this->readVarInto($varstr, $cur);
1241		} else {
1242			return $cur;
1243		}
1244	}
1245
1246	/**
1247	 * [runtime function] reads a variable into the current scope
1248	 *
1249	 * @param string $varstr the variable string, using dwoo variable syntax (i.e. "var.subvar[subsubvar]->property")
1250	 * @return mixed
1251	 */
1252	public function readVar($varstr)
1253	{
1254		if (is_array($varstr)===true) {
1255			$m = $varstr;
1256			unset($varstr);
1257		} else {
1258			if (strstr($varstr, '.') === false && strstr($varstr, '[') === false && strstr($varstr, '->') === false) {
1259				if ($varstr === 'dwoo') {
1260					return $this->globals;
1261				} elseif ($varstr === '__' || $varstr === '_root' ) {
1262					return $this->data;
1263					$varstr = substr($varstr, 6);
1264				} elseif ($varstr === '_' || $varstr === '_parent') {
1265					$varstr = '.'.$varstr;
1266					$tree = $this->scopeTree;
1267					$cur = $this->data;
1268					array_pop($tree);
1269
1270					while (($i = array_shift($tree)) !== null) {
1271						if (is_object($cur)) {
1272							$cur = $cur->$i;
1273						} else {
1274							$cur = $cur[$i];
1275						}
1276					}
1277
1278					return $cur;
1279				}
1280
1281				$cur = $this->scope;
1282
1283				if (isset($cur[$varstr])) {
1284					return $cur[$varstr];
1285				} else {
1286					return null;
1287				}
1288			}
1289
1290			if (substr($varstr, 0, 1) === '.') {
1291				$varstr = 'dwoo'.$varstr;
1292			}
1293
1294			preg_match_all('#(\[|->|\.)?([^.[\]-]+)\]?#i', $varstr, $m);
1295		}
1296
1297		$i = $m[2][0];
1298		if ($i === 'dwoo') {
1299			$cur = $this->globals;
1300			array_shift($m[2]);
1301			array_shift($m[1]);
1302			switch ($m[2][0]) {
1303
1304			case 'get':
1305				$cur = $_GET;
1306				break;
1307			case 'post':
1308				$cur = $_POST;
1309				break;
1310			case 'session':
1311				$cur = $_SESSION;
1312				break;
1313			case 'cookies':
1314			case 'cookie':
1315				$cur = $_COOKIE;
1316				break;
1317			case 'server':
1318				$cur = $_SERVER;
1319				break;
1320			case 'env':
1321				$cur = $_ENV;
1322				break;
1323			case 'request':
1324				$cur = $_REQUEST;
1325				break;
1326			case 'const':
1327				array_shift($m[2]);
1328				if (defined($m[2][0])) {
1329					return constant($m[2][0]);
1330				} else {
1331					return null;
1332				}
1333
1334			}
1335			if ($cur !== $this->globals) {
1336				array_shift($m[2]);
1337				array_shift($m[1]);
1338			}
1339		} elseif ($i === '__' || $i === '_root') {
1340			$cur = $this->data;
1341			array_shift($m[2]);
1342			array_shift($m[1]);
1343		} elseif ($i === '_' || $i === '_parent') {
1344			$tree = $this->scopeTree;
1345			$cur = $this->data;
1346
1347			while (true) {
1348				array_pop($tree);
1349				array_shift($m[2]);
1350				array_shift($m[1]);
1351				if (current($m[2]) === '_' || current($m[2]) === '_parent') {
1352					continue;
1353				}
1354
1355				while (($i = array_shift($tree)) !== null) {
1356					if (is_object($cur)) {
1357						$cur = $cur->$i;
1358					} else {
1359						$cur = $cur[$i];
1360					}
1361				}
1362				break;
1363			}
1364		} else {
1365			$cur = $this->scope;
1366		}
1367
1368		foreach ($m[1] as $k => $sep) {
1369			if ($sep === '.' || $sep === '[' || $sep === '') {
1370				if ((is_array($cur) || $cur instanceof ArrayAccess) && isset($cur[$m[2][$k]])) {
1371					$cur = $cur[$m[2][$k]];
1372				} else {
1373					return null;
1374				}
1375			} elseif ($sep === '->') {
1376				if (is_object($cur)) {
1377					$cur = $cur->$m[2][$k];
1378				} else {
1379					return null;
1380				}
1381			} else {
1382				return null;
1383			}
1384		}
1385
1386		return $cur;
1387	}
1388
1389	/**
1390	 * [runtime function] assign the value to the given variable
1391	 *
1392	 * @param mixed $value the value to assign
1393	 * @param string $scope the variable string, using dwoo variable syntax (i.e. "var.subvar[subsubvar]->property")
1394	 * @return bool true if assigned correctly or false if a problem occured while parsing the var string
1395	 */
1396	public function assignInScope($value, $scope)
1397	{
1398		$tree =& $this->scopeTree;
1399		$data =& $this->data;
1400
1401		if (!is_string($scope)) {
1402			return $this->triggerError('Assignments must be done into strings, ('.gettype($scope).') '.var_export($scope, true).' given', E_USER_ERROR);
1403		}
1404		if (strstr($scope, '.') === false && strstr($scope, '->') === false) {
1405			$this->scope[$scope] = $value;
1406		} else {
1407			// TODO handle _root/_parent scopes ?
1408			preg_match_all('#(\[|->|\.)?([^.[\]-]+)\]?#i', $scope, $m);
1409
1410			$cur =& $this->scope;
1411			$last = array(array_pop($m[1]), array_pop($m[2]));
1412
1413		        foreach ($m[1] as $k => $sep) {
1414				if ($sep === '.' || $sep === '[' || $sep === '') {
1415					if (is_array($cur) === false) {
1416						$cur = array();
1417					}
1418					$cur =& $cur[$m[2][$k]];
1419				} elseif ($sep === '->') {
1420					if (is_object($cur) === false) {
1421						$cur = new stdClass;
1422					}
1423					$cur =& $cur->$m[2][$k];
1424				} else {
1425					return false;
1426				}
1427			}
1428
1429			if ($last[0] === '.' || $last[0] === '[' || $last[0] === '') {
1430				if (is_array($cur) === false) {
1431					$cur = array();
1432				}
1433				$cur[$last[1]] = $value;
1434			} elseif ($last[0] === '->') {
1435				if (is_object($cur) === false) {
1436					$cur = new stdClass;
1437				}
1438				$cur->$last[1] = $value;
1439			} else {
1440				return false;
1441			}
1442		}
1443	}
1444
1445	/**
1446	 * [runtime function] sets the scope to the given scope string or array
1447	 *
1448	 * @param mixed $scope a string i.e. "level1.level2" or an array i.e. array("level1", "level2")
1449	 * @param bool $absolute if true, the scope is set from the top level scope and not from the current scope
1450	 * @return array the current scope tree
1451	 */
1452	public function setScope($scope, $absolute = false)
1453	{
1454		$old = $this->scopeTree;
1455
1456		if (is_string($scope)===true) {
1457			$scope = explode('.', $scope);
1458		}
1459
1460		if ($absolute===true) {
1461			$this->scope =& $this->data;
1462			$this->scopeTree = array();
1463		}
1464
1465		while (($bit = array_shift($scope)) !== null) {
1466			if ($bit === '_' || $bit === '_parent') {
1467				array_pop($this->scopeTree);
1468				$this->scope =& $this->data;
1469				$cnt = count($this->scopeTree);
1470				for ($i=0;$i<$cnt;$i++)
1471					$this->scope =& $this->scope[$this->scopeTree[$i]];
1472			} elseif ($bit === '__' || $bit === '_root') {
1473				$this->scope =& $this->data;
1474				$this->scopeTree = array();
1475			} elseif (isset($this->scope[$bit])) {
1476				$this->scope =& $this->scope[$bit];
1477				$this->scopeTree[] = $bit;
1478			} else {
1479				unset($this->scope);
1480				$this->scope = null;
1481			}
1482		}
1483
1484		return $old;
1485	}
1486
1487	/**
1488	 * [runtime function] returns the entire data array
1489	 *
1490	 * @return array
1491	 */
1492	public function getData()
1493	{
1494		return $this->data;
1495	}
1496
1497	/**
1498	 * [runtime function] returns a reference to the current scope
1499	 *
1500	 * @return &mixed
1501	 */
1502	public function &getScope()
1503	{
1504		return $this->scope;
1505	}
1506
1507	/**
1508	 * Redirects all calls to unexisting to plugin proxy.
1509	 *
1510	 * @param string Method name
1511	 * @param array  List of arguments
1512	 * @return mixed
1513	 */
1514	public function __call($method, $args) {
1515		return call_user_func_array($this->getPluginProxy()->getCallback($method), $args);
1516	}
1517}
1518