1<?php
2/*
3 * e107 website system
4 *
5 * Copyright (C) 2008-2012 e107 Inc (e107.org)
6 * Released under the terms and conditions of the
7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8 *
9*/
10
11
12/**
13 * Class e_url
14 * New v2.1.6
15 */
16class e_url
17{
18
19	private static $_instance;
20
21	private $_request       = null;
22
23	private $_config        = array();
24
25	private $_include       = null;
26
27	private $_rootnamespace = null;
28
29	private $_alias         = array();
30
31	private $_legacy        = array();
32
33	private $_legacyAliases = array();
34
35
36	/**
37	 * e_url constructor.
38	 */
39	function __construct()
40	{
41		$this->_request         = (e_HTTP === '/') ? ltrim(e_REQUEST_URI,'/') : str_replace(e_HTTP,'', e_REQUEST_URI) ;
42
43		$this->_config          = e107::getUrlConfig();
44		$this->_alias           = e107::getPref('e_url_alias');
45
46		$this->_rootnamespace   = e107::getPref('url_main_module');
47		$this->_legacy          = e107::getPref('url_config');
48		$this->_legacyAliases   = e107::getPref('url_aliases');
49
50
51		$this->setRootNamespace();
52
53	}
54
55	/**
56	 * Detect older e_url system.
57	 * @return bool
58	 */
59	private function isLegacy()
60	{
61
62		$arr = (!empty($this->_legacyAliases[e_LAN])) ?  array_merge($this->_legacy,$this->_legacyAliases[e_LAN]) : $this->_legacy;
63
64		$list = array_keys($arr);
65
66		foreach($list as $leg)
67		{
68			if(strpos($this->_request,$leg.'/') === 0 || $this->_request === $leg)
69			{
70				return true;
71			}
72
73		}
74
75		return false;
76	}
77
78
79	/**
80	 * @return string
81	 */
82	public function getInclude()
83	{
84		return $this->_include;
85	}
86
87
88
89	private function setRootNamespace()
90	{
91
92		$plugs = array_keys($this->_config);
93
94		if(!empty($this->_rootnamespace) && in_array($this->_rootnamespace,$plugs)) // Move rootnamespace check to the end of the list.
95		{
96			$v = $this->_config[$this->_rootnamespace];
97			unset($this->_config[$this->_rootnamespace]);
98			$this->_config[$this->_rootnamespace] = $v;
99		}
100
101	}
102
103
104	public function run()
105	{
106		$pref = e107::getPref();
107		$tp = e107::getParser();
108
109		if(empty($this->_config) || empty($this->_request) || $this->_request === 'index.php' || $this->isLegacy() === true)
110		{
111			return false;
112		}
113
114		$replaceAlias = array('{alias}\/?','{alias}/?','{alias}\/','{alias}/',);
115
116		foreach($this->_config as $plug=>$cfg)
117		{
118			if(empty($pref['e_url_list'][$plug])) // disabled.
119			{
120				e107::getDebug()->log('e_URL for <b>'.$plug.'</b> is disabled.');
121				continue;
122			}
123
124			foreach($cfg as $k=>$v)
125			{
126
127				if(empty($v['regex']))
128				{
129				//	e107::getMessage()->addDebug("Skipping empty regex: <b>".$k."</b>");
130					continue;
131				}
132
133
134				if(!empty($v['alias']))
135				{
136					$alias = (!empty($this->_alias[e_LAN][$plug][$k])) ? $this->_alias[e_LAN][$plug][$k] : $v['alias'];
137				//	e107::getMessage()->addDebug("e_url alias found: <b>".$alias."</b>");
138					if(!empty($this->_rootnamespace) && $this->_rootnamespace === $plug)
139					{
140						$v['regex'] = str_replace($replaceAlias, '', $v['regex']);
141					}
142					else
143					{
144
145						$v['regex'] = str_replace('{alias}', $alias, $v['regex']);
146					}
147				}
148
149
150				$regex = '#'.$v['regex'].'#';
151
152				if(empty($v['redirect']))
153				{
154					continue;
155				}
156
157
158				$newLocation = preg_replace($regex, $v['redirect'], $this->_request);
159
160				if($newLocation != $this->_request)
161				{
162					$redirect = e107::getParser()->replaceConstants($newLocation);
163					list($file,$query) = explode("?", $redirect,2);
164
165					$get = array();
166					if(!empty($query))
167					{
168						// issue #3171 fix double ampersand in case of wrong query definition
169						$query = str_replace('&&', '&', $query);
170						parse_str($query,$get);
171					}
172
173
174					foreach($get as $gk=>$gv)
175					{
176						$_GET[$gk] = $gv;
177					}
178
179					e107::getDebug()->log('e_URL in <b>'.$plug.'</b> with key: <b>'.$k.'</b> matched <b>'.$v['regex'].'</b> and included: <b>'.$file.'</b> with $_GET: '.print_a($_GET,true),1);
180
181					if(file_exists($file))
182					{
183						define('e_CURRENT_PLUGIN', $plug);
184						define('e_QUERY', str_replace('&&', '&', $query)); // do not add to e107_class.php
185						define('e_URL_LEGACY', $redirect);
186						if(!defined('e_PAGE'))
187						{
188							define('e_PAGE', basename($file));
189						}
190						$this->_include= $file;
191						return true;
192					//	exit;
193					}
194					elseif(getperms('0'))
195					{
196
197						echo "<div class='alert alert-warning'>";
198						echo "<h3>SEF Debug Info</h3>";
199						echo "File missing: ".$file;
200						echo "<br />Matched key: <b>".$k."</b>";
201						print_a($v);
202						echo "</div>";
203
204					}
205
206				}
207			}
208
209		}
210
211
212
213
214	}
215
216
217	/**
218	 * Singleton implementation
219	 * @return e_url
220	 */
221	public static function instance()
222	{
223		if(null == self::$_instance)
224		{
225		    self::$_instance = new self();
226		}
227	  	return self::$_instance;
228	}
229
230
231
232}
233
234
235
236
237
238/**
239 * e107 Front controller
240 */
241class eFront
242{
243	/**
244	 * Singleton instance
245	 * @var eFront
246	 */
247	private static $_instance;
248
249	/**
250	 * @var eDispatcher
251	 */
252	protected $_dispatcher;
253
254	/**
255	 * @var eRequest
256	 */
257	protected $_request;
258
259	/**
260	 * @var eRouter
261	 */
262	protected $_router;
263
264
265	protected $_response;
266
267	/**
268	 * @var string path to file to include - the old deprecated way of delivering content
269	 */
270	protected static $_legacy = '';
271
272	/**
273	 * Constructor
274	 */
275	private function __construct()
276	{
277	}
278
279	/**
280	 * Cloning not allowed
281	 *
282	 */
283	private function __clone()
284	{
285	}
286
287	/**
288	 * Singleton implementation
289	 * @return eFront
290	 */
291	public static function instance()
292	{
293		if(null == self::$_instance)
294		{
295		    self::$_instance = new self();
296		}
297	  	return self::$_instance;
298	}
299
300	/**
301	 * Dispatch
302	 */
303	public function dispatch(eRequest $request = null, eResponse $response = null, eDispatcher $dispatcher = null)
304	{
305		if(null === $request)
306		{
307			if(null === $this->getRequest())
308			{
309				$request = new eRequest();
310				$this->setRequest($request);
311			}
312			else $request = $this->getRequest();
313		}
314		elseif(null === $this->getRequest()) $this->setRequest($request);
315
316		if(null === $response)
317		{
318			if(null === $this->getResponse())
319			{
320				$response = new eResponse();
321				$this->setResponse($response);
322			}
323			else $response = $this->getResponse();
324		}
325		elseif(null === $this->getRequest()) $this->setRequest($request);
326
327
328		if(null === $dispatcher)
329		{
330			if(null === $this->getDispatcher())
331			{
332				$dispatcher = new eDispatcher();
333				$this->setDispatcher($dispatcher);
334			}
335			else $dispatcher = $this->getDispatcher();
336		}
337		elseif(null === $this->getDispatcher()) $this->setDispatcher($dispatcher);
338
339
340		// set dispatched status true, required for checkLegacy()
341		$request->setDispatched(true);
342
343		$router = $this->getRouter();
344
345		// If current request not already routed outside the dispatch method, route it
346		if(!$request->routed) $router->route($request);
347
348		$c = 0;
349		// dispatch loop
350		do
351		{
352			$c++;
353			if($c > 100)
354			{
355				throw new eException("Too much dispatch loops", 1);
356			}
357
358			// dispatched status true on first loop
359			$router->checkLegacy($request);
360
361			// dispatched by default - don't allow legacy to alter dispatch status
362			$request->setDispatched(true);
363
364			// legacy mod - return control to the bootstrap
365			if(self::isLegacy())
366			{
367				return;
368			}
369
370			// for the good players - dispatch loop - no more BC!
371			try
372			{
373				$dispatcher->dispatch($request, $response);
374			}
375			catch(eException $e)
376			{
377				echo $request->getRoute().' - '.$e->getMessage();
378				exit;
379			}
380
381
382		} while (!$request->isDispatched());
383	}
384
385	/**
386	 * Init all objects required for request dispatching
387	 * @return eFront
388	 */
389	public function init()
390	{
391		$request = new eRequest();
392		$this->setRequest($request);
393
394		$dispatcher = new eDispatcher();
395		$this->setDispatcher($dispatcher);
396
397		$router = new eRouter();
398		$this->setRouter($router);
399
400		/** @var eResponse $response */
401		$response = e107::getSingleton('eResponse');
402		$this->setResponse($response);
403
404		return $this;
405	}
406
407	/**
408	 * Dispatch
409	 * @param string|eRequest $route
410	 */
411	public function run($route = null)
412	{
413		if($route)
414		{
415			if(is_object($route) && ($route instanceof eRequest)) $this->setRequest($route);
416			elseif(null !== $route && null !== $this->getRequest()) $this->getRequest()->setRoute($route);
417		}
418		try
419		{
420			$this->dispatch();
421		}
422		catch(eException $e)
423		{
424			echo $e->getMessage();
425			exit;
426		}
427
428	}
429
430	/**
431	 * Application instance (e107 class)
432	 * @return e107
433	 */
434	public static function app()
435	{
436		return e107::getInstance();
437	}
438
439	/**
440	 * Get dispatcher instance
441	 * @return eDispatcher
442	 */
443	public function getDispatcher()
444	{
445		return $this->_dispatcher;
446	}
447
448	/**
449	 * Set dispatcher
450	 * @param eDispatcher $dispatcher
451	 * @return eFront
452	 */
453	public function setDispatcher(eDispatcher $dispatcher)
454	{
455		$this->_dispatcher = $dispatcher;
456		return $this;
457	}
458
459	/**
460	 * Get request instance
461	 * @return eRequest
462	 */
463	public function getRequest()
464	{
465		return $this->_request;
466	}
467
468	/**
469	 * Set request
470	 * @param eRequest $request
471	 * @return eFront
472	 */
473	public function setRequest(eRequest $request)
474	{
475		$this->_request = $request;
476		return $this;
477	}
478
479	/**
480	 * Get response instance
481	 * @return eResponse
482	 */
483	public function getResponse()
484	{
485		return $this->_response;
486	}
487
488	/**
489	 * Set response
490	 * @param eResponse $response
491	 * @return eFront
492	 */
493	public function setResponse(eResponse $response)
494	{
495		$this->_response = $response;
496		return $this;
497	}
498
499	/**
500	 * Get router instance
501	 * @return eRouter
502	 */
503	public function getRouter()
504	{
505		return $this->_router;
506	}
507
508	/**
509	 * Set router instance
510	 * @return eFront
511	 */
512	public function setRouter(eRouter $router)
513	{
514		$this->_router = $router;
515		return $this;
516	}
517
518	/**
519	 * Set/get legacy status of the current request
520	 * @param boolean $status
521	 * @return boolean
522	 */
523	public static function isLegacy($status = null)
524	{
525		if(null !== $status)
526		{
527			if(!empty($status[0]) && ($status[0] === '{'))
528			{
529				$status = e107::getParser()->replaceConstants($status);
530			}
531			self::$_legacy = $status;
532		}
533		return self::$_legacy;
534	}
535}
536
537/**
538 * e107 Dispatcher
539 * It decides how to dispatch the request.
540 */
541class eDispatcher
542{
543	protected static $_configObjects = array();
544
545	public function dispatch(eRequest $request = null, eResponse $response = null)
546	{
547		$controllerName = $request->getControllerName();
548		$moduleName = $request->getModuleName();
549		$className = $this->isDispatchable($request, false);
550
551		// dispatch based on rule settings
552		if(!$className)
553		{
554			if($controllerName == 'index') // v2.x upgrade has not been run yet.
555			{
556				e107::getRedirect()->redirect(e_ADMIN."admin.php");
557			}
558
559			throw new eException("Invalid controller '".$controllerName."'");
560		}
561
562		$controller = new $className($request, $response);
563		if(!($controller instanceof eController))
564		{
565			throw new eException("Controller $controller is not an instance of eController");
566		}
567
568		$actionName = $request->getActionMethodName();
569
570		ob_start();
571
572		$controller->dispatch($actionName);
573
574		$content = ob_get_contents();
575		ob_end_clean();
576
577		$response->appendBody($content);
578		unset($controller);
579	}
580
581	/**
582	 * Get path to the e_url handler
583	 * @param string $module
584	 * @param string $location plugin|core|override
585	 * @param boolean $sc
586	 * @return string path
587	 */
588	public static function getConfigPath($module, $location, $sc = false)
589	{
590		$tmp = explode('/', $location);
591		$custom = '';
592		$location = $tmp[0];
593		if(isset($tmp[1]) && !empty($tmp[1]))
594		{
595			$custom = $tmp[1].'_';
596		}
597		unset($tmp);
598		if($module !== '*') $module .= '/';
599
600		switch ($location)
601		{
602			case 'plugin':
603				//if($custom) $custom = 'url/'.$custom;
604				if(!defined('e_CURRENT_PLUGIN'))
605				{
606					define('e_CURRENT_PLUGIN', rtrim($module,'/')); // TODO Move to a better location.
607				}
608				return $sc ? '{e_PLUGIN}'.$module.'url/'.$custom.'url.php' : e_PLUGIN.$module.'url/'.$custom.'url.php';
609			break;
610
611			case 'core':
612				if($module === '*') return $sc ? '{e_CORE}url/' : e_CORE.'url/';
613				return $sc ? '{e_CORE}url/'.$module.$custom.'url.php' : e_CORE.'url/'.$module.$custom.'url.php';
614			break;
615
616			case 'override':
617				if($module === '*') return $sc ? '{e_CORE}override/url/'  : e_CORE.'override/url/' ;
618				return $sc ? '{e_CORE}override/url/'.$module.$custom.'url.php'  : e_CORE.'override/url/'.$module.$custom.'url.php' ;
619			break;
620
621			default:
622				return null;
623			break;
624		}
625	}
626
627	/**
628	 * Get path to url configuration subfolders
629	 * @param string $module
630	 * @param string $location plugin|core|override
631	 * @param boolean $sc
632	 * @return string path
633	 */
634	public static function getConfigLocationPath($module, $location, $sc = false)
635	{
636		switch ($location)
637		{
638			case 'plugin':
639				return $sc ? '{e_PLUGIN}'.$module.'/url/' : e_PLUGIN.$module.'/url/';
640			break;
641
642			case 'core':
643				return $sc ? '{e_CORE}url/'.$module.'/' : e_CORE.'url/'.$module.'/';
644			break;
645
646			case 'override':
647				return $sc ? '{e_CORE}override/url/'.$module.'/'  : e_CORE.'override/url/'.$module.'/';
648			break;
649
650			default:
651				return null;
652			break;
653		}
654	}
655
656	/**
657	 * Get dispatch system path
658	 * @param string $location plugin|core|override
659	 * @param string $plugin required only when $location is plugin
660	 * @param boolean $sc
661	 * @return string path
662	 */
663	public static function getDispatchLocationPath($location, $plugin = null, $sc = false)
664	{
665		switch ($location)
666		{
667			case 'plugin':
668				if(!$plugin) return null;
669				return $sc ? '{e_PLUGIN}'.$plugin.'/controllers/' : e_PLUGIN.$plugin.'/controllers/';
670			break;
671
672			case 'core':
673				return $sc ? '{e_CORE}controllers/' : e_CORE.'controllers/';
674			break;
675
676			case 'override':
677				return $sc ? '{e_CORE}override/controllers/' : e_CORE.'override/controllers/';
678			break;
679
680			default:
681				return null;
682			break;
683		}
684	}
685
686	/**
687	 * Get full dispatch system path
688	 * @param string $module
689	 * @param string $location plugin|core|override
690	 * @param boolean $sc
691	 * @return string path
692	 */
693	public static function getDispatchPath($module, $location, $sc = false)
694	{
695		switch ($location)
696		{
697			case 'plugin':
698				return $sc ? '{e_PLUGIN}'.$module.'/controllers/' : e_PLUGIN.$module.'/controllers/';
699			break;
700
701			case 'core':
702				return $sc ? '{e_CORE}controllers/'.$module.'/' : e_CORE.'controllers/'.$module.'/';
703			break;
704
705			case 'override':
706				return $sc ? '{e_CORE}override/controllers/'.$module.'/' : e_CORE.'override/controllers/'.$module.'/';
707			break;
708
709			default:
710				return null;
711			break;
712		}
713	}
714
715	/**
716	 * Get include path to a given module/controller
717	 *
718	 * @param string $module valid module name
719	 * @param string $controller controller name
720	 * @param string $location core|plugin|override
721	 * @param boolean $sc return relative (false) OR shortcode (true) path
722	 * @return string controller path
723	 */
724	public static function getControllerPath($module, $controller, $location = null, $sc = false)
725	{
726		if(null === $location) $location = self::getDispatchLocation($module);
727
728		return ($location ? self::getDispatchPath($module, $location, $sc).$controller.'.php': null);
729	}
730
731	/**
732	 * Get class name of a given module/controller
733	 *
734	 * @param string $module valid module name
735	 * @param string $controllerName controller name
736	 * @param string $location core|plugin|override
737	 * @return string controller path
738	 */
739	public static function getControllerClass($module, $controllerName, $location = null)
740	{
741		if(null === $location) $location = self::getDispatchLocation($module);
742
743		return ($location ? $location.'_'.$module.'_'.$controllerName.'_controller' : null);
744	}
745
746
747	/**
748	 * Get controller object
749	 *
750	 * @param eRequest $request
751	 * @param boolean $checkOverride whether to check the override location
752	 * @return eController null if not dispatchable
753	 */
754	public function getController(eRequest $request, $checkOverride = true)
755	{
756		$class_name = $this->isDispatchable($request, true, $checkOverride);
757		if(!$class_name) return null;
758
759		return new $class_name();
760	}
761
762	/**
763	 * Check if given module/controller is dispatchable
764	 * @param string $module valid module name
765	 * @param string $controllerName controller name
766	 * @param string $location core|plugin|override
767	 * @param boolean $checkOverride whether to check the override location
768	 * @return string class name OR false if not dispatchable
769	 */
770	public function isDispatchableModule($module, $controllerName, $location, $checkOverride = false)
771	{
772		if($checkOverride || $location == 'override')
773		{
774			$path = self::getControllerPath($module, $controllerName, 'override', false);
775
776			$class_name = self::getControllerClass($module, $controllerName, 'override');
777			if($class_name && !class_exists($class_name, false) && is_readable($path)) include_once($path);
778
779			if($class_name && class_exists($class_name, false)) return $class_name;
780		}
781
782		// fallback to original dispatch location if any
783		if($location === 'override')
784		{
785			// check for real location
786			if(($location = eDispatcher::getModuleRealLocation($module)) === null) return false;
787		}
788
789		if($location !== 'override')
790		{
791			$path = self::getControllerPath($module, $controllerName, $location, false);
792
793			$class_name = self::getControllerClass($module, $controllerName, $location);
794			if(!$class_name) return false;
795
796			if(!class_exists($class_name, false) && is_readable($path)) include_once($path);
797
798			if(class_exists($class_name, false)) return $class_name;
799		}
800		return false;
801	}
802
803	/**
804	 * Automated version of self::isDispatchableModule()
805	 * @param eRequest $request
806	 * @param boolean $checkReflection deep check - proper subclassing, action
807	 * @param boolean $checkOverride try override controller folder first
808	 * @return mixed class name OR false if not dispatchable
809	 */
810	public function isDispatchable(eRequest $request, $checkReflection = false, $checkOverride = true)
811	{
812		$location = self::getDispatchLocation($request->getModuleName());
813
814		$controllerName = $request->getControllerName();
815		$moduleName = $request->getModuleName();
816		$className = false;
817
818		// dispatch based on url_config preference value, if config location is override and there is no
819		// override controller, additional check against real controller location will be made
820		if($location)
821		{
822			$className = $this->isDispatchableModule($moduleName, $controllerName, $location, $checkOverride);
823		}
824		//else
825		//{
826			# Disable plugin check for routes with no config info - prevent calling of non-installed plugins
827			# We may allow this for plugins which don't have plugin.xml in the future
828			// $className = $this->isDispatchableModule($moduleName, $controllerName, 'plugin', $checkOverride);
829			// if(!$className)
830			//$className = $this->isDispatchableModule($moduleName, $controllerName, 'core', $checkOverride);
831		//}
832
833		if(empty($className)) return false;
834		elseif(!$checkReflection) return $className;
835
836		$rfl = new ReflectionClass($className);
837		$method = $request->getActionMethodName();
838		if($rfl->isSubclassOf('eController') && $rfl->hasMethod($method) && $rfl->getMethod($method)->isPublic() && !$rfl->getMethod($method)->isStatic())
839			return $className;
840
841		return false;
842	}
843
844	/**
845	 * Get class name of a given module config
846	 *
847	 * @param string $module valid module name
848	 * @param string $location core|plugin|override[/custom]
849	 * @return string controller path
850	 */
851	public static function getConfigClassName($module, $location)
852	{
853		$tmp = explode('/', $location);
854		$custom = '';
855		$location = $tmp[0];
856		if(isset($tmp[1]) && !empty($tmp[1]))
857		{
858			$custom = $tmp[1].'_';
859		}
860		unset($tmp);
861		$module .= '_';
862
863		// we need to prepend location to avoid namespace colisions
864		return $location.'_'.$module.$custom.'url';
865	}
866
867	/**
868	 * Get config object for a module
869	 *
870	 * @param string $module valid module name
871	 * @param string $location core|plugin|override[/custom]
872	 * @return eUrlConfig
873	 */
874	public static function getConfigObject($module, $location = null)
875	{
876		if(null === $location)
877		{
878			$location = self::getModuleConfigLocation($module);
879			if(!$location) return null;
880		}
881		$reg = $module.'/'.$location;
882		if(isset(self::$_configObjects[$reg])) return self::$_configObjects[$reg];
883		$className = self::getConfigClassName($module, $location);
884		if(!class_exists($className, false))
885		{
886			$path = self::getConfigPath($module, $location, false);
887			if(!is_readable($path)) return null;
888			include_once($path);
889
890			if(!class_exists($className, false)) return null;
891		}
892
893		/** @var eUrlConfig $obj */
894		$obj = new $className();
895		$obj->init();
896		self::$_configObjects[$reg] = $obj;
897		$obj = null;
898
899		return self::$_configObjects[$reg];
900	}
901
902	/**
903	 * Auto discover module location from stored in core prefs data
904	 * @param string $module
905	 * @return mixed
906	 */
907	public static function getModuleConfigLocation($module)
908	{
909		//retrieve from config prefs
910		return e107::findPref('url_config/'.$module, '');
911	}
912
913	/**
914	 * Auto discover module location from stored in core prefs data
915	 * @param string $module
916	 * @return mixed|null|string
917	 */
918	public static function getDispatchLocation($module)
919	{
920		//retrieve from prefs
921		$location = self::getModuleConfigLocation($module);
922		if(!$location) return null;
923
924		if(($pos = strpos($location, '/'))) //can't be 0
925		{
926			return substr($location, 0, $pos);
927		}
928		return $location;
929	}
930
931
932	/**
933	 * Auto discover module real location (and not currently set from url adminsitration) from stored in core prefs data
934	 * @param string $module
935	 */
936	public static function getModuleRealLocation($module)
937	{
938		//retrieve from prefs
939		$searchArray = e107::findPref('url_modules');
940		if(!$searchArray) return null;
941
942		$search = array('core', 'plugin', 'override');
943
944		foreach ($search as $location)
945		{
946			$_searchArray = vartrue($searchArray[$location], array());
947			if(in_array($module, $_searchArray)) return $location;
948		}
949		return null;
950	}
951}
952
953/**
954 * URL manager - parse and create URLs based on rules set
955 * Inspired by Yii Framework UrlManager <www.yiiframework.com>
956 */
957class eRouter
958{
959	/**
960	 * Configuration array containing all available syste routes and route object configuration values
961	 * @var array
962	 */
963	protected $_rules = array();
964
965	/**
966	 * List of all system wide available aliases
967	 * This includes multi-lingual configurations as well
968	 * @var array
969	 */
970	protected $_aliases = array();
971
972	/**
973	 * Cache for rule objects
974	 * @var array
975	 */
976	protected $_parsedRules = array(); // array of rule objects
977
978	/**
979	 * Global config values per rule set
980	 * @var array
981	 */
982	protected $_globalConfig = array();
983
984	/**
985	 * Module name which is used for site main namespace
986	 * Example mysite.com/news/News Item => converted to mysite.com/News Item
987	 * NOTE: Could be moved to rules config
988	 *
989	 * @var string
990	 */
991	protected $_mainNsModule = '';
992
993	/**
994	 * Default URL suffix - to be added to end of all urls (e.g. '.html')
995	 * This value can be overridden per rule item
996	 * NOTE could be moved to rules config only
997	 * @var string
998	 */
999	public $urlSuffix = '';
1000
1001	/**
1002	 * @var string  GET variable name for route. Defaults to 'route'.
1003	 */
1004	public $routeVar = 'route';
1005
1006	/**
1007	 * @var string
1008	 */
1009	const FORMAT_GET = 'get';
1010
1011	/**
1012	 * @var string
1013	 */
1014	const FORMAT_PATH = 'path';
1015
1016	/**
1017	 * @var string
1018	 */
1019	private $_urlFormat = self::FORMAT_PATH;
1020
1021	/**
1022	 * Not found route
1023	 * @var string
1024	 */
1025	public $notFoundRoute = 'system/error/notfound';
1026
1027	protected $_defaultAssembleOptions = array('full' => false, 'amp' => '&amp;', 'equal' => '=', 'encode' => true);
1028
1029	/**
1030	 * Not found URL - used when system route not found and 'url_error_redirect' core pref is true
1031	 * TODO - user friendly URL ('/system/404') when system config is ready ('/system/404')
1032	 * @var string
1033	 */
1034	public $notFoundUrl = 'system/error/404?type=routeError';
1035
1036
1037
1038
1039	public function __construct()
1040	{
1041		$this->_init();
1042	}
1043
1044	/**
1045	 * Init object
1046	 * @return void
1047	 */
1048	protected function _init()
1049	{
1050		// Gather all rules, add-on info, cache, module for main namespace etc
1051		$this->_loadConfig()
1052			->setAliases();
1053		// we need config first as setter does some checks if module can be set as main
1054		$this->setMainModule(e107::getPref('url_main_module', ''));
1055	}
1056
1057	/**
1058	 * Set module for default namespace
1059	 * @param string $module
1060	 * @return eRouter
1061	 */
1062	public function setMainModule($module)
1063	{
1064		if(!$module || !$this->isModule($module) || !$this->getConfigValue($module, 'allowMain')) return $this;
1065		$this->_mainNsModule = $module;
1066		return $this;
1067	}
1068
1069	/**
1070	 * Get main url namespace module
1071	 * @return string
1072	 */
1073	public function getMainModule()
1074	{
1075		return $this->_mainNsModule;
1076	}
1077
1078	/**
1079	 * Check if given module is the main module
1080	 * @param string $module
1081	 * @return boolean
1082	 */
1083	public function isMainModule($module)
1084	{
1085		return ($this->_mainNsModule === $module);
1086	}
1087
1088
1089	/**
1090	 * @return string get|path
1091	 */
1092	public function getUrlFormat()
1093	{
1094		return $this->_urlFormat;
1095	}
1096
1097
1098	/**
1099	 * Load config and url rules, if not available - build it on the fly
1100	 * @return eRouter
1101	 */
1102	protected function _loadConfig()
1103	{
1104		if(!is_readable(e_CACHE_URL.'config.php')) $config = $this->buildGlobalConfig();
1105		else $config = include(e_CACHE_URL.'config.php');
1106
1107		if(!$config) $config = array();
1108
1109		$rules = array();
1110
1111		foreach ($config as $module => $c)
1112		{
1113			$rules[$module] = $c['rules'];
1114			unset($config[$module]['rules']);
1115			$config[$module] = $config[$module]['config'];
1116		}
1117		$this->_globalConfig = $config;
1118		$this->setRuleSets($rules);
1119
1120		return $this;
1121	}
1122
1123	public static function clearCache()
1124	{
1125		if(file_exists(e_CACHE_URL.'config.php'))
1126		{
1127			@unlink(e_CACHE_URL.'config.php');
1128		}
1129	}
1130
1131	/**
1132	 * Build unified config.php
1133	 */
1134	public function buildGlobalConfig($save = true)
1135	{
1136		$active = e107::getPref('url_config', array());
1137
1138		$config = array();
1139		foreach ($active as $module => $location)
1140		{
1141			$_config = array();
1142			$obj = eDispatcher::getConfigObject($module, $location);
1143			$path = eDispatcher::getConfigPath($module, $location, true);
1144
1145			if(null !== $obj)
1146			{
1147				$_config = $obj->config();
1148				$_config['config']['configPath'] = $path;
1149				$_config['config']['configClass'] = eDispatcher::getConfigClassName($module, $location);
1150			}
1151			if(!isset($_config['config'])) $_config['config'] = array();
1152
1153			$_config['config']['location'] = $location;
1154			if(!isset($_config['config']['format']) || !in_array($_config['config']['format'], array(self::FORMAT_GET, self::FORMAT_PATH)))
1155			{
1156				$_config['config']['format'] = $this->getUrlFormat();
1157			}
1158
1159			if(!isset($_config['rules'])) $_config['rules'] = array();
1160
1161			foreach ($_config['rules'] as $pattern => $rule)
1162			{
1163				if(!is_array($rule))
1164				{
1165					$_config['rules'][$pattern] = array($rule);
1166				}
1167			}
1168
1169			$config[$module] = $_config;
1170		}
1171
1172		if($save)
1173		{
1174			$fileContent = '<?php'."\n### Auto-generated - DO NOT EDIT ### \nreturn ";
1175			$fileContent .= trim(var_export($config, true)).';';
1176
1177			file_put_contents(e_CACHE_URL.'config.php', $fileContent);
1178		}
1179		return $config;
1180	}
1181
1182	/**
1183	 * Retrieve config array from a given system path
1184	 * @param string $path
1185	 * @param string $location core|plugin|override
1186	 */
1187	public static function adminReadConfigs($path, $location = null)
1188	{
1189		$file = e107::getFile(false);
1190		$ret = array();
1191
1192		$file->mode = 'fname';
1193		$files = $file->setFileInfo('fname')
1194			->get_files($path, '^([a-z_]{1,}_)?url\.php$');
1195
1196
1197		foreach ($files as $file)
1198		{
1199			if(null === $location)
1200			{
1201				$c = eRouter::file2config($file, $location);
1202				if($c) $ret[] = $c;
1203				continue;
1204			}
1205			$ret[] = eRouter::file2config($file, $location);
1206		}
1207		return $ret;
1208	}
1209
1210	/**
1211	 * Convert filename to configuration string
1212	 * @param string $filename
1213	 * @param string $location core|plugin|override
1214	 */
1215	public static function file2config($filename, $location = '')
1216	{
1217		if($filename == 'url.php') return $location;
1218		if($location) $location .= '/';
1219		return $location.substr($filename, 0, strrpos($filename, '_'));
1220	}
1221
1222	/**
1223	 * Detect all available system url modules, used as a map on administration configuration path
1224	 * and required (same structure) {@link from eDispatcher::adminBuildConfig())
1225	 * This is a very liberal detection, as it doesn't require config file.
1226	 * It goes through both config and dispatch locations and registers directory tree as modules
1227	 * The only exception are plugins - if plugin requires install (plugin.xml) and it is not installed,
1228	 * it won't be registered
1229	 * Another important thing is - core has always higher priority, as plugins are not allowed to
1230	 * directly override core modules. At this moment, core modules could be overloaded only via override configs (e107_core/override/url/)
1231	 * and controllers (e107_core/override/controllers)
1232	 * This array is stored as url_modules core preference
1233	 *
1234	 * @param string $type possible values are all|plugin|core|override
1235	 * @return array available system url modules stored as url_modules core preference
1236	 */
1237	public static function adminReadModules($type = 'all')
1238	{
1239		$f = e107::getFile();
1240		$ret = array('core' => array(), 'plugin' => array(), 'override' => array());
1241
1242		if($type == 'all' || $type = 'core')
1243		{
1244			$location = eDispatcher::getDispatchLocationPath('core');
1245			// search for controllers first
1246			$ret['core'] = $f->get_dirs($location);
1247
1248			// merge with configs
1249			$configArray = $f->get_dirs(eDispatcher::getConfigPath('*', 'core'));
1250			foreach ($configArray as $config)
1251			{
1252				if(!in_array($config, $ret['core']))
1253				{
1254					$ret['core'][] = $config;
1255				}
1256			}
1257			sort($ret['core']);
1258		}
1259
1260		if($type == 'all' || $type = 'plugin')
1261		{
1262			$plugins = $f->get_dirs(e_PLUGIN);
1263			foreach ($plugins as $plugin)
1264			{
1265				// DON'T ALLOW PLUGINS TO OVERRIDE CORE!!!
1266				// This will be possible in the future under some other, more controllable form
1267				if(in_array($plugin, $ret['core'])) continue;
1268
1269				$location = eDispatcher::getDispatchLocationPath('plugin', $plugin);
1270				$config = eDispatcher::getConfigPath($plugin, 'plugin');
1271
1272				if(e107::isInstalled($plugin))
1273				{
1274					if(is_dir($location) || is_readable($config))
1275					{
1276						$ret['plugin'][] = $plugin;
1277					}
1278					continue;
1279				}
1280
1281				// Register only those who don't need install and may be dispatchable
1282				if((!is_readable(e_PLUGIN.$plugin.'/plugin.php') && !is_readable(e_PLUGIN.$plugin.'/plugin.xml')))
1283				{
1284					if(is_dir($location) || is_readable($config))
1285					{
1286						$ret['plugin'][] = $plugin;
1287					}
1288				}
1289			}
1290			sort($ret['plugin']);
1291		}
1292
1293		if($type == 'all' || $type = 'override')
1294		{
1295			// search for controllers first
1296			$location = eDispatcher::getDispatchLocationPath('override');
1297			$ret['override'] = $f->get_dirs($location);
1298
1299			// merge with configs
1300			$configArray = $f->get_dirs(eDispatcher::getConfigPath('*', 'override'));
1301			foreach ($configArray as $config)
1302			{
1303				if(!in_array($config, $ret['override']))
1304				{
1305					$ret['override'][] = $config;
1306				}
1307			}
1308			sort($ret['override']);
1309
1310			// remove not installed plugin locations, possible only for 'all' type
1311			if($type == 'all')
1312			{
1313				foreach ($ret['override'] as $i => $l)
1314				{
1315					// it's a plugin override, but not listed in current plugin array - remove
1316					if(in_array($l, $plugins) && !in_array($l, $ret['plugin']))
1317					{
1318						unset($ret['override'][$i]);
1319					}
1320				}
1321			}
1322		}
1323
1324		return $ret;
1325	}
1326
1327	/**
1328	 * Rebuild configuration array, stored as url_config core preference
1329	 * More strict detection compared to {@link eDispatcher::adminReadModules()}
1330	 * Current flat array containing config locations per module are rebuilt so that new
1331	 * modules are registered, missing modules - removed. Additionally fallback to the default location
1332	 * is done if current user defined location is not readable
1333	 * @see eDispatcher::adminReadModules()
1334	 * @param array current configuration array (url_config core preference like)
1335	 * @param array available URL modules as detected by {@link eDispatcher::adminReadModules()} and stored as url_modules core preference value
1336	 * @return array new url_config array
1337	 */
1338	public static function adminBuildConfig($current, $adminReadModules = null)
1339	{
1340		if(null === $adminReadModules) $adminReadModules = self::adminReadModules();
1341
1342		$ret = array();
1343		$all = array_unique(array_merge($adminReadModules['core'], $adminReadModules['plugin'], $adminReadModules['override']));
1344		foreach ($all as $module)
1345		{
1346			if(isset($current[$module]))
1347			{
1348				// current contains custom (readable) config location e.g. news => core/rewrite
1349				if(strpos($current[$module], '/') !== false && is_readable(eDispatcher::getConfigPath($module, $current[$module])))
1350				{
1351					$ret[$module] = $current[$module];
1352					continue;
1353				}
1354
1355				// in all other cases additional re-check will be made - see below
1356			}
1357
1358			if(in_array($module, $adminReadModules['override']))
1359			{
1360				// core check
1361				if(in_array($module, $adminReadModules['core']))
1362				{
1363					$mustHave = is_readable(eDispatcher::getConfigPath($module, 'core'));
1364					$has = is_readable(eDispatcher::getConfigPath($module, 'override'));
1365
1366					// No matter if it must have, it has e_url config
1367					if($has) $ret[$module] = 'override';
1368					// It must have but it doesn't have e_url config, fallback
1369					elseif($mustHave && !$has) $ret[$module] = 'core';
1370					// Rest is always core as controller override is done on run time
1371					else $ret[$module] = 'core';
1372				}
1373				// plugin check
1374				elseif(in_array($module, $adminReadModules['plugin']))
1375				{
1376					$mustHave = is_readable(eDispatcher::getConfigPath($module, 'plugin'));
1377					$has = is_readable(eDispatcher::getConfigPath($module, 'override'));
1378
1379					// No matter if it must have, it has e_url config
1380					if($has) $ret[$module] = 'override';
1381					// It must have but it doesn't have e_url config, fallback
1382					elseif($mustHave && !$has) $ret[$module] = 'plugin';
1383					// Rest is always plugin as config is most important, controller override check is done on run time
1384					else $ret[$module] = 'plugin';
1385				}
1386				// standalone override module
1387				else
1388				{
1389					$ret[$module] = 'override';
1390				}
1391
1392			}
1393			// default core location
1394			elseif(in_array($module, $adminReadModules['core']))
1395			{
1396				$ret[$module] = 'core';
1397			}
1398			// default plugin location
1399			elseif(in_array($module, $adminReadModules['plugin']))
1400			{
1401				$ret[$module] = 'plugin';
1402			}
1403		}
1404		return $ret;
1405	}
1406
1407	/**
1408	 * Detect available config locations (readable check), based on available url_modules {@link eDispatcher::adminReadModules()} core preference arrays
1409	 * Used to rebuild url_locations core preference value
1410	 * @see eDispatcher::adminBuildConfig()
1411	 * @see eDispatcher::adminReadModules()
1412	 * @param array $available {@link eDispatcher::adminReadModules()} stored as url_modules core preference
1413	 * @return array available config locations, stored as url_locations core preference
1414	 */
1415	public static function adminBuildLocations($available = null)
1416	{
1417		$ret = array();
1418		if(null === $available) $available = self::adminReadModules();
1419
1420		$fl = e107::getFile();
1421
1422		// Core
1423		foreach ($available['core'] as $module)
1424		{
1425			// Default module
1426			$ret[$module] = array('core');
1427
1428			// read sub-locations
1429			$path = eDispatcher::getConfigLocationPath($module, 'core');
1430			//$sub = $fl->get_dirs($path);
1431			$sub = eRouter::adminReadConfigs($path);
1432
1433			if($sub)
1434			{
1435				foreach ($sub as $moduleSub)
1436				{
1437					// auto-override: override available (controller or url config), check for config
1438					if(in_array($module, $available['override']) && is_readable(eDispatcher::getConfigPath($module, 'override/'.$moduleSub)))
1439					{
1440						$ret[$module][] = 'override/'.$moduleSub;
1441					}
1442					// no override available, register the core location
1443					elseif(is_readable(eDispatcher::getConfigPath($module, 'core/'.$moduleSub)))
1444					{
1445						$ret[$module][] = 'core/'.$moduleSub;
1446					}
1447				}
1448			}
1449		}
1450
1451
1452		// Plugins
1453		foreach ($available['plugin'] as $module)
1454		{
1455			// Default module
1456			$ret[$module] = array('plugin');
1457
1458			// read sub-locations
1459			$path = eDispatcher::getConfigLocationPath($module, 'plugin');
1460			//$sub = $fl->get_dirs($path);
1461			$sub = eRouter::adminReadConfigs($path);
1462
1463			if($sub)
1464			{
1465				foreach ($sub as $moduleSub)
1466				{
1467					// auto-override: override available (controller or url config), check for config
1468					if(in_array($module, $available['override']) && is_readable(eDispatcher::getConfigPath($module, 'override/'.$moduleSub)))
1469					{
1470						$ret[$module][] = 'override/'.$moduleSub;
1471					}
1472					// no override available, register the core location
1473					elseif(is_readable(eDispatcher::getConfigPath($module, 'plugin/'.$moduleSub)))
1474					{
1475						$ret[$module][] = 'plugin/'.$moduleSub;
1476					}
1477				}
1478			}
1479		}
1480
1481		// Go through all overrides, register those who don't belong to core & plugins as standalone core modules
1482		foreach ($available['override'] as $module)
1483		{
1484			// either it is a core/plugin module or e_url.php is not readable - continue
1485			if(in_array($module, $available['core']) || in_array($module, $available['plugin']))
1486			{
1487				continue;
1488			}
1489
1490			// Default module
1491			$ret[$module] = array('override');
1492
1493			// read sub-locations
1494			$path = eDispatcher::getConfigLocationPath($module, 'override');
1495			//$sub = $fl->get_dirs($path);
1496			$sub = eRouter::adminReadConfigs($path);
1497
1498			if($sub)
1499			{
1500				foreach ($sub as $moduleSub)
1501				{
1502					if(is_readable(eDispatcher::getConfigPath($module, 'override/'.$moduleSub)))
1503					{
1504						$ret[$module][] = 'override/'.$moduleSub;
1505					}
1506				}
1507			}
1508		}
1509
1510		return $ret;
1511	}
1512
1513	/**
1514	 * Match current aliases against currently available module and languages
1515	 * @param array $currentAliases url_aliases core preference
1516	 * @param array $currentConfig url_config core preference
1517	 * @return array cleaned aliases
1518	 */
1519	public static function adminSyncAliases($currentAliases, $currentConfig)
1520	{
1521		if(empty($currentAliases)) return array();
1522
1523		$modules = array_keys($currentConfig);
1524
1525		// remove non existing languages
1526		$lng = e107::getLanguage();
1527		$lanList = $lng->installed();
1528
1529		if(is_array($currentAliases))
1530		{
1531			foreach ($currentAliases as $lanCode => $aliases)
1532			{
1533				$lanName = $lng->convert($lanCode);
1534				if(!$lanName || !in_array($lanName, $lanList))
1535				{
1536					unset($currentAliases[$lanCode]);
1537					continue;
1538				}
1539
1540				// remove non-existing modules
1541				foreach ($aliases as $alias => $module)
1542				{
1543					if(!isset($currentConfig[$module])) unset($currentAliases[$lanCode][$alias]);
1544				}
1545			}
1546		}
1547		return $currentAliases;
1548	}
1549
1550	/**
1551	 * Retrieve global configuration array for a single or all modules
1552	 * @param string $module system module
1553	 * @return array configuration
1554	 */
1555	public function getConfig($module = null)
1556	{
1557		if(null === $module) return $this->_globalConfig;
1558
1559		return isset($this->_globalConfig[$module]) ? $this->_globalConfig[$module] : array();
1560	}
1561
1562	/**
1563	 * Retrieve single value from a module global configuration array
1564	 * @param string $module system module
1565	 * @return array configuration
1566	 */
1567	public function getConfigValue($module, $key, $default = null)
1568	{
1569		return isset($this->_globalConfig[$module]) && isset($this->_globalConfig[$module][$key]) ? $this->_globalConfig[$module][$key] : $default;
1570	}
1571
1572	/**
1573	 * Get system name of a module by its alias
1574	 * Returns null if $alias is not an existing alias
1575	 * @param string $alias
1576	 * @param string $lan optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
1577	 * @return string module
1578	 */
1579	public function getModuleFromAlias($alias, $lan = null)
1580	{
1581		if($lan) return e107::findPref('url_aliases/'.$lan.'/'.$alias, null);
1582		return (isset($this->_aliases[$alias]) ? $this->_aliases[$alias] : null);
1583	}
1584
1585	/**
1586	 * Get alias name for a module
1587	 * Returns null if module doesn't have an alias
1588	 * @param string $module
1589	 * @param string $lan optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
1590	 * @return string alias
1591	 */
1592	public function getAliasFromModule($module, $lan = null)
1593	{
1594		if($lan)
1595		{
1596			$aliases = e107::findPref('url_aliases/'.$lan, array());
1597			return (in_array($module, $aliases) ? array_search($module, $aliases) : null);
1598		}
1599		return (in_array($module, $this->_aliases) ? array_search($module, $this->_aliases) : null);
1600	}
1601
1602	/**
1603	 * Check if alias exists
1604	 * @param string $alias
1605	 * @param string $lan optional language alias. Example $lan = 'bg' (search for Bulgarian aliases)
1606	 * @return boolean
1607	 */
1608	public function isAlias($alias, $lan = null)
1609	{
1610		if($lan)
1611		{
1612			$aliases = e107::findPref('url_aliases/'.$lan, array());
1613			return isset($aliases[$alias]);
1614		}
1615		return isset($this->_aliases[$alias]);
1616	}
1617
1618	/**
1619	 * Check if there is an alias for provided module
1620	 * @param string $module
1621	 * @param string $lan optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
1622	 * @return boolean
1623	 */
1624	public function hasAlias($module, $lan = null)
1625	{
1626		if($lan)
1627		{
1628			$aliases = e107::findPref('url_aliases/'.$lan, array());
1629			return in_array($module, $aliases);
1630		}
1631		return in_array($module, $this->_aliases);
1632	}
1633
1634	/**
1635	 * Get all available module aliases
1636	 * @param string $lan optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
1637	 * @return array
1638	 */
1639	public function getAliases($lanCode = null)
1640	{
1641		if($lanCode)
1642		{
1643			return e107::findPref('url_aliases/'.$lanCode, array());
1644		}
1645		return $this->_aliases;
1646	}
1647
1648	/**
1649	 * Set module aliases
1650	 * @param array $aliases
1651	 * @return eRouter
1652	 */
1653	public function setAliases($aliases = null)
1654	{
1655		if(null === $aliases)
1656		{
1657			$lanCode = e107::getLanguage()->convert(e_LANGUAGE);
1658
1659			$aliases = e107::findPref('url_aliases/'.$lanCode, array());
1660		}
1661		$this->_aliases = $aliases;
1662
1663		return $this;
1664	}
1665
1666	/**
1667	 * Check if provided module is present in the rules config
1668	 * @param string module
1669	 * @return boolean
1670	 */
1671	public function isModule($module)
1672	{
1673		return isset($this->_globalConfig[$module]);
1674	}
1675
1676	/**
1677	 * Check if the passed value is valid module or module alias, returns system module
1678	 * or null on failure
1679	 * @param string $module
1680	 * @param boolean $strict check for existence if true
1681	 * @return string module
1682	 */
1683	public function retrieveModule($module, $strict = true)
1684	{
1685		if($this->isAlias($module))
1686			$module = $this->getModuleFromAlias($module);
1687
1688		if($strict && (!$module || !$this->isModule($module)))
1689			return null;
1690
1691		return $module;
1692	}
1693
1694	/**
1695	 * Set rule config for this instance
1696	 * @param array $rules
1697	 * @return void
1698	 */
1699	public function setRuleSets($rules)
1700	{
1701		$this->_rules = $rules;
1702	}
1703
1704	/**
1705	 * Retrieve rule set for a module
1706	 * @param string $module
1707	 */
1708	public function getRuleSet($module)
1709	{
1710		return (isset($this->_rules[$module]) ? $this->_rules[$module] : array());
1711	}
1712
1713	/**
1714	 * Get all rule sets
1715	 */
1716	public function getRuleSets()
1717	{
1718		return $this->_rules;
1719	}
1720
1721	/**
1722	 * Retrive array of eUrlRule objects for given module
1723	 */
1724	public function getRules($module)
1725	{
1726		return $this->_processRules($module);
1727	}
1728
1729	/**
1730	 * Process rule set array, create rule objects
1731	 * TODO - rule cache
1732	 * @param string $module
1733	 * @return array processed rule set
1734	 */
1735	protected function _processRules($module)
1736	{
1737		if(!$this->isModule($module)) return array();
1738
1739		if(!isset($this->_parsedRules[$module]))
1740		{
1741			$rules = $this->getRuleSet($module);
1742			$config = $this->getConfig($module);
1743			$this->_parsedRules[$module] = array();
1744			$map = array('urlSuffix' => 'urlSuffix', 'legacy' => 'legacy', 'legacyQuery' => 'legacyQuery', 'mapVars' => 'mapVars', 'allowVars' => 'allowVars', 'matchValue' => 'matchValue');
1745			foreach ($rules as $pattern => $set)
1746			{
1747				foreach ($map as $key => $value)
1748				{
1749					if(!isset($set[$value]) && isset($config[$key]))
1750					{
1751						$set[$value] = $config[$key];
1752					}
1753				}
1754				$this->_parsedRules[$module][$pattern] = $this->createRule($set, $pattern);
1755			}
1756		}
1757		return $this->_parsedRules[$module];
1758	}
1759
1760	/**
1761	 * Create rule object
1762	 *
1763	 * @param string $route
1764	 * @param string|array $pattern
1765	 * @param boolean $cache
1766	 * @return eUrlRule
1767	 */
1768	protected function createRule($route, $pattern = null, $cache = false)
1769	{
1770		return new eUrlRule($route, $pattern, $cache);
1771	}
1772
1773
1774	private function _debug($label,$val=null, $line=null)
1775	{
1776		if(!deftrue('e_DEBUG_SEF'))
1777		{
1778			return false;
1779		}
1780
1781		e107::getDebug()->log("<h3>SEF: ".$label . " <small>".basename(__FILE__)." (".$line.")</small></h3>".print_a($val,true));
1782	}
1783
1784	/**
1785	 * Route current request
1786	 * @param eRequest $request
1787	 * @return boolean
1788	 */
1789	public function route(eRequest $request, $checkOnly = false)
1790	{
1791		$request->routed = false;
1792
1793		if(isset($_GET[$this->routeVar]))
1794		{
1795			$rawPathInfo = $_GET[$this->routeVar];
1796			unset($_GET[$this->routeVar]);
1797			$this->_urlFormat = self::FORMAT_GET;
1798		}
1799		else
1800		{
1801			$rawPathInfo = rawurldecode($request->getPathInfo());
1802			//$this->_urlFormat = self::FORMAT_PATH;
1803		}
1804
1805
1806
1807		// Ignore social trackers when determining route.
1808		$get = eHelper::removeTrackers($_GET);
1809
1810		// Route to front page - index/index/index route
1811		if(!$rawPathInfo && (!$this->getMainModule() || empty($get)))
1812		{
1813			// front page settings will be detected and front page will be rendered
1814			$request->setRoute('index/index/index');
1815			$request->addRouteHistory($rawPathInfo);
1816			$request->routed = true;
1817			return true;
1818		}
1819
1820		// max number of parts is actually 4 - module/controller/action/[additional/pathinfo/vars], here for reference only
1821		$parts = $rawPathInfo ? explode('/', $rawPathInfo, 4) : array();
1822
1823		$this->_debug('parts',$parts,  __LINE__);
1824
1825		// find module - check aliases
1826		$module = $this->retrieveModule($parts[0]);
1827		$mainSwitch = false;
1828
1829		// no module found, switch to Main module (pref) if available
1830		if(null === $module && $this->getMainModule() && $this->isModule($this->getMainModule()))
1831		{
1832			$module = $this->getMainModule();
1833			$rawPathInfo = $module.'/'.$rawPathInfo;
1834			array_unshift($parts, $module);
1835			$mainSwitch = true;
1836		}
1837
1838		$request->routePathInfo = $rawPathInfo;
1839
1840		$this->_debug('module',$module,  __LINE__);
1841		$this->_debug('rawPathInfo',$rawPathInfo,  __LINE__);
1842
1843
1844		// valid module
1845		if(null !== $module)
1846		{
1847			// we have valid module
1848			$config = $this->getConfig($module);
1849
1850			$this->_debug('config',$module,  __LINE__);
1851
1852			// set legacy state
1853			eFront::isLegacy(varset($config['legacy']));
1854
1855			// Don't allow single entry if required by module config
1856			if(vartrue($config['noSingleEntry']))
1857			{
1858				$request->routed = true;
1859				if(!eFront::isLegacy())
1860				{
1861					$request->setRoute($this->notFoundRoute);
1862					return false;
1863				}
1864				// legacy entry point - include it later in the bootstrap, legacy query string will be set to current
1865				$request->addRouteHistory($rawPathInfo);
1866				return true;
1867			}
1868
1869			// URL format - the one set by current config overrides the auto-detection
1870			$format = isset($config['format']) && $config['format'] ? $config['format'] : $this->getUrlFormat();
1871
1872			//remove leading module, unnecessary overhead while matching
1873			array_shift($parts);
1874			$rawPathInfo = $parts ? implode('/', $parts) : '';
1875			$pathInfo = $this->removeUrlSuffix($rawPathInfo, $this->urlSuffix);
1876
1877			// retrieve rules if any and if needed
1878			$rules = $format == self::FORMAT_PATH ? $this->getRules($module) : array();
1879
1880			// Further parsing may still be needed
1881			if(empty($rawPathInfo))
1882			{
1883				$rawPathInfo = $pathInfo;
1884			}
1885
1886			// parse callback
1887			if(vartrue($config['selfParse']))
1888			{
1889				// controller/action[/additional/parms]
1890				if(vartrue($config['urlSuffix'])) $rawPathInfo = $this->removeUrlSuffix($rawPathInfo, $config['urlSuffix']);
1891				$route = $this->configCallback($module, 'parse', array($rawPathInfo, $_GET, $request, $this, $config), $config['location']);
1892			}
1893			// default module route
1894			elseif($format == self::FORMAT_GET || !$rules)
1895			{
1896				$route = $pathInfo;
1897			}
1898			// rules available - try to match an Url Rule
1899			elseif($rules)
1900			{
1901			//	$this->_debug('rules',$rules,  __LINE__);
1902
1903				foreach ($rules as $rule)
1904				{
1905					$route = $rule->parseUrl($this, $request, $pathInfo, $rawPathInfo);
1906
1907
1908
1909					if($route !== false)
1910					{
1911						eFront::isLegacy($rule->legacy); // legacy include override
1912
1913						$this->_debug('rule->legacy',$rule->legacy,  __LINE__);
1914						$this->_debug('rule->parseCallback',$rule->parseCallback,  __LINE__);
1915
1916						if($rule->parseCallback)
1917						{
1918							$this->configCallback($module, $rule->parseCallback, array($request), $config['location']);
1919						}
1920
1921						// parse legacy query string if any
1922						$this->_debug('rule->legacyQuery',$rule->legacyQuery,  __LINE__);
1923
1924						if(null !== $rule->legacyQuery)
1925						{
1926							$obj = eDispatcher::getConfigObject($module, $config['location']);
1927							// eUrlConfig::legacyQueryString set as legacy string by default in eUrlConfig::legacy() method
1928							$vars = new e_vars($request->getRequestParams());
1929							$vars->module = $module;
1930							$vars->controller = $request->getController();
1931							$vars->action = $request->getAction();
1932							if($rule->allowVars)
1933							{
1934								foreach ($rule->allowVars as $key)
1935								{
1936									if(isset($_GET[$key]) && !$request->isRequestParam($key))
1937									{
1938										// sanitize
1939										$vars->$key = preg_replace('/[^\d\w\-]/', '', $_GET[$key]);
1940									}
1941								}
1942							}
1943							$obj->legacyQueryString = e107::getParser()->simpleParse($rule->legacyQuery, $vars, '0');
1944
1945							$this->_debug('obj->legacyQueryString',$obj->legacyQueryString,  __LINE__);
1946							unset($vars, $obj);
1947						}
1948						break;
1949					}
1950				}
1951			}
1952
1953			// append module to be registered in the request object
1954			if(false !== $route)
1955			{
1956				// don't modify if true - request directly modified by config callback
1957				if(!$request->routed)
1958				{
1959					if(eFront::isLegacy()) $this->configCallback($module, 'legacy', array($route, $request), $config['location']);
1960					$route = $module.'/'.$route;
1961				}
1962			}
1963			// No route found, we didn't switched to main module auto-magically
1964			elseif(!$mainSwitch && vartrue($config['errorRoute']))
1965			{
1966				$route = !$checkOnly ? $module.'/'.$config['errorRoute'] : false;
1967			}
1968
1969		}
1970
1971		// final fallback
1972		if(!vartrue($route))
1973		{
1974			if($request->routed)
1975			{
1976				$route = $request->getRoute();
1977			}
1978
1979			if(!$route)
1980			{
1981				$route = $this->notFoundRoute;
1982				eFront::isLegacy(''); // reset legacy - not found route isn't legacy call
1983				$request->routed = true;
1984				if($checkOnly) return false;
1985				## Global redirect on error option
1986				if(e107::getPref('url_error_redirect', false) && $this->notFoundUrl)
1987				{
1988					$redirect = $this->assemble($this->notFoundUrl, '', 'encode=0&full=1');
1989					//echo $redirect; exit;
1990					e107::getRedirect()->redirect($redirect, true, 404);
1991				}
1992			}
1993		}
1994
1995		$this->_debug('route',$route,  __LINE__);
1996
1997		$request->setRoute($route);
1998		$request->addRouteHistory($route);
1999		$request->routed = true;
2000		return true;
2001	}
2002
2003	/**
2004	 * And more BC
2005	 * Checks and does some addtional logic if registered module is of type legacy
2006	 * @param eRequest $request
2007	 * @return void
2008	 */
2009	public function checkLegacy(eRequest $request)
2010	{
2011		$module = $request->getModule();
2012
2013		// forward from controller to a legacy module - bad stuff
2014		if(!$request->isDispatched() && $this->getConfigValue($module, 'legacy'))
2015		{
2016			eFront::isLegacy($this->getConfigValue($module, 'legacy'));
2017
2018			$url = $this->assemble($request->getRoute(), $request->getRequestParams());
2019			$request->setRequestInfo($url)->setPathInfo(null)->setRoute(null);
2020
2021			$_GET = $request->getRequestParams();
2022			$_SERVER['QUERY_STRING'] = http_build_query($request->getRequestParams(), null, '&');
2023
2024			// Infinite loop impossible, as dispatcher will break because of the registered legacy path
2025			$this->route($request);
2026		}
2027	}
2028
2029	/**
2030	 * Convenient way to call config methods
2031	 */
2032	public function configCallback($module, $callBack, $params, $location)
2033	{
2034		if(null == $location) $location = eDispatcher::getModuleConfigLocation($module);
2035		if(!$module || !($obj = eDispatcher::getConfigObject($module, $location))) return false;
2036
2037		return call_user_func_array(array($obj, $callBack), $params);
2038	}
2039
2040	/**
2041	 * Convert assembled url to shortcode
2042	 *
2043	 * @param string $route
2044	 * @param array $params
2045	 * @param array $options {@see eRouter::$_defaultAssembleOptions}
2046	 */
2047	public function assembleSc($route, $params = array(), $options = array())
2048	{
2049		//if(is_string($options)) parse_str($options, $options);
2050		$url = $this->assemble($route, $params, $options);
2051		return e107::getParser()->createConstants($url, 'mix');
2052	}
2053
2054	/**
2055	 * Assemble system URL
2056	 * Examples:
2057	 * <?php
2058	 * $router->assemble('/'); // index page URL e.g. / or /site_folder/
2059	 * $router->assemble('news/view/item?id=1'); // depends on current news config, possible return value is /news/1
2060	 * $router->assemble('*', 'id=1'); // use current request info - /module/controller/action?id=1
2061	 * $router->assemble('* /* /newaction'); // (NO EMPTY SPACES) change only current action - /module/controller/newaction
2062	 * $newsItem = array('news_id' => 1, 'news_sef' => 'My Title', ...); // as retrieved from DB
2063	 * $router->assemble('news/view/item', $newsItem); // All unused key=>values will be removed and NOT appended as GET vars
2064	 *
2065	 * @param string $route
2066	 * @param array $params
2067	 * @param array $options {@see eRouter::$_defaultAssembleOptions}
2068	 */
2069	public function assemble($route, $params = array(), $options = array())
2070	{
2071		// TODO - url options
2072		$request = eFront::instance()->getRequest();
2073		if(is_string($options)) parse_str($options, $options);
2074		$options = array_merge($this->_defaultAssembleOptions, $options);
2075		$base = ($options['full'] ? SITEURLBASE : '').$request->getBasePath();
2076
2077		$anc = '';
2078
2079
2080		if(is_string($params)) parse_str($params, $params);
2081		if(isset($params['#']))
2082		{
2083			$anc = '#'.$params['#'];
2084			unset($params['#']);
2085		}
2086
2087		// Config independent - Deny parameter keys, useful for directly denying sensitive data e.g. password db fields
2088		if(isset($options['deny']))
2089		{
2090			$list = array_map('trim', explode(',', $options['deny']));
2091			foreach ($list as $value)
2092			{
2093				unset($params[$value]);
2094			}
2095			unset($list);
2096		}
2097
2098		// Config independent - allow parameter keys, useful to directly allow data (and not to rely on config allowVars) e.g. when retrieved from db
2099		if(isset($options['allow']))
2100		{
2101			$list = array_map('trim', explode(',', $options['allow']));
2102			$_params = $params;
2103			$params = array();
2104			foreach ($list as $value)
2105			{
2106				if(isset($_params[$value])) $params[$value] = $_params[$value];
2107			}
2108			unset($list, $_params);
2109		}
2110
2111		# Optional convenient masks for creating system URL's
2112		if($route === '/' || empty($route))
2113		{
2114			if($params)
2115			{
2116				$params = $this->createPathInfo($params, $options);
2117				return $base.'?'.$params;
2118			}
2119			return $base;
2120		}
2121		elseif(strpos($route, '?') !== false)
2122		{
2123			$tmp = explode('?', $route, 2);
2124			$route = $tmp[0];
2125			parse_str($tmp[1], $params);
2126			unset($tmp);
2127		}
2128
2129		if($route === '*')
2130		{
2131			$route = $route = explode('/', $request->getRoute());
2132		}
2133		elseif(strpos($route, '*') !== false)
2134		{
2135			$route = explode('/', $route, 3);
2136			if($route[0] === '*') $route[0] = $request->getModule();
2137			if(isset($route[1]) && $route[1] === '*') $route[1] = $request->getController();
2138		}
2139		else
2140		{
2141			$route = explode('/', $route, 3);
2142		}
2143
2144		// we don't know anything about this route, just build it blind
2145		if(!$this->isModule($route[0]))
2146		{
2147			if($params)
2148			{
2149				$params = $this->createPathInfo($params, $options);
2150				return $base.implode('/', $route).'?'.$params;
2151			}
2152			return $base.implode('/', $route);
2153		}
2154
2155		# fill in index when needed - XXX not needed, may be removed soon
2156		switch (count($route))
2157		{
2158			case 1:
2159				$route[1] = 'index';
2160				$route[2] = 'index';
2161			break;
2162			case 2:
2163				$route[2] = 'index';
2164			break;
2165		}
2166
2167		# aliases
2168		$module = $route[0];
2169		$config = $this->getConfig($module);
2170
2171		$alias = $this->hasAlias($module, vartrue($options['lan'], null)) ? $this->getAliasFromModule($module, vartrue($options['lan'], null)) : $module;
2172		$route[0] = $alias;
2173		if($options['encode']) $alias = rawurlencode($alias);
2174
2175		$format = isset($config['format']) && $config['format'] ? $config['format'] : self::FORMAT_GET;
2176
2177		$urlSuffix = '';
2178
2179		// Fix base url for legacy links
2180		if(vartrue($config['noSingleEntry'])) $base = $options['full'] ? SITEURL : e_HTTP;
2181		elseif(self::FORMAT_GET !== $config['format'])
2182		{
2183			$urlSuffix = $this->urlSuffix;
2184			if(isset($config['urlSuffix'])) $urlSuffix = $config['urlSuffix'];
2185		}
2186
2187		// Create by config callback
2188		if(vartrue($config['selfCreate']))
2189		{
2190			$tmp = $this->configCallback($module, 'create', array(array($route[1], $route[2]), $params, $options), $config['location']);
2191
2192			if(empty($tmp)) return '#not-found';
2193
2194			if(is_array($tmp))
2195			{
2196				$route = $tmp[0];
2197				$params = $tmp[1];
2198
2199				if($options['encode']) $route = array_map('rawurlencode', $route);
2200				$route = implode('/', $route);
2201
2202				if(!$route)
2203				{
2204					$urlSuffix = '';
2205					if(!$this->isMainModule($module)) $route = $alias;
2206				}
2207				elseif (!$this->isMainModule($module))
2208				{
2209					$route = $alias.'/'.$route;
2210				}
2211
2212			}
2213			else
2214			{
2215				// relative url returned
2216				return $base.$tmp.$anc;
2217			}
2218			unset($tmp);
2219
2220			if($format === self::FORMAT_GET)
2221			{
2222				$params[$this->routeVar] = $route;
2223				$route = '';
2224			}
2225
2226			if($params)
2227			{
2228				$params = $this->createPathInfo($params, $options);
2229				return $base.$route.$urlSuffix.'?'.$params.$anc;
2230			}
2231
2232			return $base.$route.$urlSuffix.$anc;
2233		}
2234
2235
2236		// System URL create routine
2237		$rules = $this->getRules($module);
2238		if($format !== self::FORMAT_GET && !empty($rules))
2239		{
2240			foreach ($rules as $k => $rule)
2241			{
2242				if (($url = $rule->createUrl($this, array($route[1], $route[2]), $params, $options)) !== false)
2243				{
2244					 return $base.rtrim(($this->isMainModule($module) ? '' : $alias.'/').$url, '/').$anc;
2245				}
2246			}
2247		}
2248
2249		// default - module/controller/action
2250		if($this->isMainModule($module)) unset($route[0]);
2251		if($route[2] == 'index')
2252		{
2253			unset($route[2]);
2254			if($route[1] == 'index') unset($route[1]);
2255		}
2256
2257		# Modify params if required
2258		if($params)
2259		{
2260			if(varset($config['mapVars']))
2261			{
2262				foreach ($config['mapVars'] as $srcKey => $dstKey)
2263				{
2264					if (isset($params[$srcKey]))
2265					{
2266						$params[$dstKey] = $params[$srcKey];
2267						unset($params[$srcKey]);
2268					}
2269				}
2270			}
2271
2272			// false means - no vars are allowed, nothing to preserve here
2273			if(varset($config['allowVars']) === false) $params = array();
2274			// default empty array value - try to guess what's allowed - mapVars is the best possible candidate
2275			elseif(empty($config['allowVars']) && !empty($config['mapVars'])) $params = array_unique(array_values($config['mapVars']));
2276			// disallow everything but valid URL parameters
2277			if(!empty($config['allowVars']))
2278			{
2279				$copy = $params;
2280				$params = array();
2281				foreach ($config['allowVars'] as $key)
2282				{
2283					if(isset($copy[$key])) $params[$key] = $copy[$key];
2284				}
2285				unset($copy);
2286			}
2287
2288			if($format === self::FORMAT_GET)
2289			{
2290				$urlSuffix = '';
2291				$copy = $params;
2292				$params = array();
2293				$params[$this->routeVar] = implode('/', $route);
2294				foreach ($copy as $key => $value)
2295				{
2296					$params[$key] = $value;
2297				}
2298				unset($copy);
2299				$route = array();
2300			}
2301			$params = $this->createPathInfo($params, $options);
2302			$route = implode('/', $route);
2303			if(!$route || $route == $alias) $urlSuffix = '';
2304			return $base.$route.$urlSuffix.'?'.$params.$anc;
2305		}
2306		$route = implode('/', $route);
2307		if(!$route || $route == $alias) $urlSuffix = '';
2308
2309
2310		return $format === self::FORMAT_GET ? $base.'?'.$this->routeVar.'='.$route.$anc : $base.$route.$urlSuffix.$anc;
2311	}
2312
2313	/**
2314	 * Alias of assemble()
2315	 */
2316	public function url($route, $params = array())
2317	{
2318		return $this->assemble($route, $params);
2319	}
2320
2321	/**
2322	 * Creates a path info based on the given parameters.
2323	 * XXX - maybe we can switch to http_build_query(), should be able to do everything we need in a much better way
2324	 *
2325	 * @param array $params list of GET parameters
2326	 * @param array $options rawurlencode, equal, encode and amp settings
2327	 * @param string $key this is used internally for recursive calls
2328	 *
2329	 * @return string the created path info
2330	 */
2331	public function createPathInfo($params, $options, $key = null)
2332	{
2333		$pairs = array();
2334		$equal = $options['equal'];
2335		$encode = $options['encode'];
2336		$ampersand = !$encode && $options['amp'] == '&amp;' ? '&' : $options['amp'];
2337		foreach ($params as $k => $v)
2338		{
2339			if (null !== $key) $k = $key.'['.rawurlencode($k).']';
2340
2341			if (is_array($v)) $pairs[] = $this->createPathInfo($v, $options, $k);
2342			else
2343			{
2344				if(null === $v)
2345				{
2346					if($encode)
2347					{
2348						$k = null !== $key ? $k : rawurlencode($k);
2349					}
2350					$pairs[] = $k;
2351					continue;
2352				}
2353				if($encode)
2354				{
2355					$k =  null !== $key ? $k : rawurlencode($k);
2356					$v = rawurlencode($v);
2357				}
2358				$pairs[] = $k.$equal.$v;
2359			}
2360		}
2361		return implode($ampersand, $pairs);
2362	}
2363
2364	/**
2365	 * Parses a path info into URL segments
2366	 * Be sure to not use non-unique chars for equal and ampersand signs, or you'll break your URLs
2367	 *
2368	 * @param eRequest $request
2369	 * @param string $pathInfo path info
2370	 * @param string $equal
2371	 * @param string $ampersand
2372	 */
2373	public function parsePathInfo($pathInfo, $equal = '/', $ampersand = '/')
2374	{
2375		if ('' === $pathInfo) return;
2376
2377		if ($equal != $ampersand) $pathInfo = str_replace($equal, $ampersand, $pathInfo);
2378		$segs = explode($ampersand, $pathInfo.$ampersand);
2379
2380		$segs = explode('/', $pathInfo);
2381		$ret = array();
2382
2383		for ($i = 0, $n = count($segs); $i < $n - 1; $i += 2)
2384		{
2385			$key = $segs[$i];
2386			if ('' === $key) continue;
2387			$value = $segs[$i + 1];
2388			// array support
2389			if (($pos = strpos($key, '[')) !== false && ($pos2 = strpos($key, ']', $pos + 1)) !== false)
2390			{
2391				$name = substr($key, 0, $pos);
2392				// numerical array
2393				if ($pos2 === $pos + 1)
2394					$ret[$name][] = $value;
2395				// associative array
2396				else
2397				{
2398					$key = substr($key, $pos + 1, $pos2 - $pos - 1);
2399					$ret[$name][$key] = $value;
2400				}
2401			}
2402			else
2403			{
2404				$ret[$key] = $value;
2405
2406			}
2407		}
2408		return $ret;
2409	}
2410
2411	/**
2412	 * Removes the URL suffix from path info.
2413	 * @param string $pathInfo path info part in the URL
2414	 * @param string $urlSuffix the URL suffix to be removed
2415	 *
2416	 * @return string path info with URL suffix removed.
2417	 */
2418	public function removeUrlSuffix($pathInfo, $urlSuffix)
2419	{
2420		if ('' !== $urlSuffix && substr($pathInfo, -strlen($urlSuffix)) === $urlSuffix) return substr($pathInfo, 0, -strlen($urlSuffix));
2421		else return $pathInfo;
2422	}
2423}
2424
2425class eException extends Exception
2426{
2427
2428}
2429
2430/**
2431 * Based on Yii Framework UrlRule handler <www.yiiframework.com>
2432 */
2433class eUrlRule
2434{
2435	/**
2436	 *
2437	 * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
2438	 * Defaults to null, meaning using the value of {@link cl_shop_core_url::urlSuffix}.
2439	 *
2440	 * @var string the URL suffix used for this rule.
2441	 */
2442	public $urlSuffix;
2443
2444	/**
2445	 * When this rule is used to parse the incoming request, the values declared in this property
2446	 * will be injected into $_GET.
2447	 *
2448	 * @var array the default GET parameters (name=>value) that this rule provides.
2449	 */
2450	public $defaultParams = array();
2451
2452	/**
2453	 * @var string module/controller/action
2454	 */
2455	public $route;
2456
2457	/**
2458	 * @var array the mapping from route param name to token name (e.g. _r1=><1>)
2459	 */
2460	public $references = array();
2461
2462	/**
2463	 * @var string the pattern used to match route
2464	 */
2465	public $routePattern;
2466
2467	/**
2468	 * @var string regular expression used to parse a URL
2469	 */
2470	public $pattern;
2471
2472	/**
2473	 * @var string template used to construct a URL
2474	 */
2475	public $template;
2476
2477	/**
2478	 * @var array list of parameters (name=>regular expression)
2479	 */
2480	public $params = array();
2481
2482	/**
2483	 * @var boolean whether the URL allows additional parameters at the end of the path info.
2484	 */
2485	public $append;
2486
2487	/**
2488	 * @var array list of SourceKey=>DestinationKey associations
2489	 */
2490	public $mapVars = array();
2491
2492	/**
2493	 * Numerical array of allowed parameter keys. If set, everything else will be wiped out from the passed parameter array
2494	 * @var array
2495	 */
2496	public $allowVars = array();
2497
2498	/**
2499	 * Should be values matched vs route patterns when assembling URLs
2500	 * Warning SLOW when true!!!
2501	 * @var mixed true or 1 for preg_match (extremely slower), or 'empty' for only empty check (better)
2502	 */
2503	public $matchValue;
2504
2505	/**
2506	 * Method member of module config object, to be called after successful request parsing
2507	 * @var string
2508	 */
2509	public $parseCallback;
2510
2511	/**
2512	 * Shortcode path to the old entry point e.g. '{e_BASE}news.php'
2513	 * @var string
2514	 */
2515	public $legacy;
2516
2517	/**
2518	 * Template used for automated recognition of legacy QueryString (parsed via simpleParser with values of retrieved requestParameters)
2519	 * @var string
2520	 */
2521	public $legacyQuery;
2522
2523	/**
2524	 * Core regex templates
2525	 * Example usage - route <var:{number}> will result in
2526	 * @var array
2527	 */
2528	public $regexTemplates = array(
2529		'az'					=> '[A-Za-z]+', // NOTE - it won't match non-latin word characters!
2530		'alphanum'  			=> '[\w\pL]+',
2531		'sefsecure' 			=> '[\w\pL\s\-+.,]+',
2532		'secure' 				=> '[^\/\'"\\<%]+',
2533		'number' 				=> '[\d]+',
2534		'username' 				=> '[\w\pL.\-\s!,]+', // TODO - should equal to username pattern, sync it
2535		'azOptional'			=> '[A-Za-z]{0,}',
2536		'alphanumOptional'  	=> '[\w\pL]{0,}',
2537		'sefsecureOptional' 	=> '[\w\pL\s\-+.,]{0,}',
2538		'secureOptional' 		=> '[^\/\'"\\<%]{0,}',
2539		'numberOptional' 		=> '[\d]{0,}',
2540		'usernameOptional' 		=> '[\w\pL.\-\s!,]{0,}', // TODO - should equal to username pattern, sync it
2541	);
2542
2543	/**
2544	 * User defined regex templates
2545	 * @var array
2546	 */
2547	public $varTemplates = array();
2548
2549	/**
2550	 * All regex templates
2551	 * @var e_var
2552	 */
2553	protected $_regexTemplates;
2554
2555
2556	/**
2557	 * Constructor.
2558	 * @param string $route the route of the URL (controller/action)
2559	 * @param string $pattern the pattern for matching the URL
2560	 */
2561	public function __construct($route, $pattern, $fromCache = false)
2562	{
2563		if (is_array($route))
2564		{
2565			if ($fromCache && !$pattern)
2566			{
2567				$this->setData($route);
2568				$this->_regexTemplates = new e_vars($this->regexTemplates);
2569				return;
2570			}
2571
2572			$this->setData($route);
2573			if($this->defaultParams && is_string($this->defaultParams))
2574			{
2575				parse_str($this->defaultParams, $this->defaultParams);
2576			}
2577			$route = $this->route = $route[0];
2578		}
2579		else $this->route = $route;
2580
2581		$tr2['/'] = $tr['/'] = '\\/';
2582
2583		if (strpos($route, '<') !== false && preg_match_all('/<(\w+)>/', $route, $matches2))
2584		{
2585			foreach ($matches2[1] as $name) $this->references[$name] = "<$name>";
2586		}
2587
2588		if($this->varTemplates)
2589		{
2590			// don't override core regex templates
2591			$this->regexTemplates = array_merge($this->varTemplates, $this->regexTemplates);
2592			$this->varTemplates = array();
2593		}
2594		$this->_regexTemplates = new e_vars($this->regexTemplates);
2595
2596		if (preg_match_all('/<(\w+):?(.*?)?>/', $pattern, $matches))
2597		{
2598			$tokens = array_combine($matches[1], $matches[2]);
2599			$tp = e107::getParser();
2600			foreach ($tokens as $name => $value)
2601			{
2602				if ($value === '') $value = '[^\/]+';
2603				elseif($value[0] == '{')
2604				{
2605					$value = $tp->simpleParse($value, $this->_regexTemplates, '[^\/]+');
2606				}
2607				$tr["<$name>"] = "(?P<$name>$value)";
2608				if (isset($this->references[$name])) $tr2["<$name>"] = $tr["<$name>"];
2609				else $this->params[$name] = $value;
2610			}
2611		}
2612
2613		$p = rtrim($pattern, '*');
2614		$this->append = $p !== $pattern;
2615		$p = trim($p, '/');
2616		$this->template = preg_replace('/<(\w+):?.*?>/', '<$1>', $p);
2617		$this->pattern = '/^'.strtr($this->template, $tr).'\/?';
2618		if ($this->append) $this->pattern .= '/u';
2619		else $this->pattern .= '$/u';
2620
2621		if ($this->references !== array()) $this->routePattern = '/^'.strtr($this->route, $tr2).'$/u';
2622	}
2623
2624	public function getData()
2625	{
2626		$vars = array_keys(get_class_vars(__CLASS__));
2627		$data = array();
2628		foreach ($vars as $prop)
2629		{
2630			$data[$prop] = $this->$prop;
2631		}
2632		return $data;
2633	}
2634
2635	protected function setData($data)
2636	{
2637		if (!is_array($data)) return;
2638		$vars = array_keys(get_class_vars(__CLASS__));
2639
2640		foreach ($vars as $prop)
2641		{
2642			if (!isset($data[$prop])) continue;
2643			$this->$prop = $data[$prop];
2644		}
2645	}
2646
2647	/**
2648	 * Creates a URL based on this rule.
2649	 * TODO - more clear logic and flexibility by building the query string
2650	 *
2651	 * @param eRouter $manager the router/manager
2652	 * @param string $route the route
2653	 * @param array $params list of parameters
2654	 * @param array $options
2655	 * @return mixed the constructed URL or false on error
2656	 */
2657	public function createUrl($manager, $route, $params, $options)
2658	{
2659		$case = 'i';
2660		$ampersand = $options['amp'];
2661		$encode = vartrue($options['encode']);
2662
2663		if(is_array($route)) $route = implode('/', $route);
2664
2665
2666
2667		$tr = array();
2668		if ($route !== $this->route)
2669		{
2670			if ($this->routePattern !== null && preg_match($this->routePattern.$case, $route, $matches))
2671			{
2672				foreach ($this->references as $key => $name) $tr[$name] = $matches[$key];
2673			}
2674			else return false;
2675		}
2676
2677		// map vars first
2678		foreach ($this->mapVars as $srcKey => $dstKey)
2679		{
2680			if (isset($params[$srcKey])/* && !isset($params[$dstKey])*/)
2681			{
2682				$params[$dstKey] = $params[$srcKey];
2683				unset($params[$srcKey]);
2684			}
2685		}
2686
2687		// false means - no vars are allowed, preserve only route vars
2688		if($this->allowVars === false) $this->allowVars = array_keys($this->params);
2689		// empty array (default) - everything is allowed
2690
2691		// disallow everything but valid URL parameters
2692		if(!empty($this->allowVars))
2693		{
2694			$copy = $params;
2695			$params = array();
2696			$this->allowVars = array_unique(array_merge($this->allowVars, array_keys($this->params)));
2697			foreach ($this->allowVars as $key)
2698			{
2699				if(isset($copy[$key])) $params[$key] = $copy[$key];
2700			}
2701			unset($copy);
2702		}
2703
2704		foreach ($this->defaultParams as $key => $value)
2705		{
2706			if (isset($params[$key]))
2707			{
2708				if ($params[$key] == $value) unset($params[$key]);
2709				else return false;
2710			}
2711		}
2712
2713		foreach ($this->params as $key => $value) if (!isset($params[$key])) return false;
2714
2715		if($this->matchValue)
2716		{
2717
2718			if('empty' !== $this->matchValue)
2719			{
2720				foreach($this->params as $key=>$value)
2721				{
2722					if(!preg_match('/'.$value.'/'.$case,$params[$key]))
2723						return false;
2724				}
2725			}
2726			else
2727			{
2728				foreach($this->params as $key=>$value)
2729				{
2730					if(empty($params[$key]) )
2731						return false;
2732				}
2733			}
2734		}
2735
2736		$tp = e107::getParser();
2737		$urlFormat = e107::getConfig()->get('url_sef_translate');
2738
2739		foreach ($this->params as $key => $value)
2740		{
2741			// FIX - non-latin URLs proper encoded
2742			$tr["<$key>"] = rawurlencode($params[$key]); //todo transliterate non-latin
2743		//	$tr["<$key>"] = eHelper::title2sef($tp->toASCII($params[$key]), $urlFormat); // enabled to test.
2744			unset($params[$key]);
2745		}
2746
2747		$suffix = $this->urlSuffix === null ? $manager->urlSuffix : $this->urlSuffix;
2748
2749		// XXX TODO Find better place for this check which will affect all types of SEF URL configurations. (@see news/sef_noid_url.php for duplicate)
2750
2751
2752
2753
2754		if($urlFormat == 'dashl' || $urlFormat == 'underscorel' || $urlFormat == 'plusl') // convert template to lowercase when using lowercase SEF URL format.
2755		{
2756			$this->template = strtolower($this->template);
2757		}
2758
2759		$url = strtr($this->template, $tr);
2760
2761		// Work-around fix for lowercase username
2762		if($urlFormat == 'dashl' && $this->route == 'profile/view')
2763		{
2764			$url = str_replace('%20','-', strtolower($url));
2765		}
2766
2767		if(empty($params))
2768		{
2769			 return $url !== '' ? $url.$suffix : $url;
2770		}
2771
2772		// apppend not supported, maybe in the future...?
2773		if ($this->append) $url .= '/'.$manager->createPathInfo($params, '/', '/').$suffix;
2774		else
2775		{
2776			if ($url !== '') $url = $url.$suffix;
2777
2778			$options['equal'] = '=';
2779			$url .= '?'.$manager->createPathInfo($params, $options);
2780		}
2781
2782
2783		return rtrim($url, '/');
2784	}
2785
2786	/**
2787	 * Parases a URL based on this rule.
2788	 * @param eRouter $manager the router/URL manager
2789	 * @param eRequest $request the request object
2790	 * @param string $pathInfo path info part of the URL
2791	 * @param string $rawPathInfo path info that contains the potential URL suffix
2792	 * @return mixed the route that consists of the controller ID and action ID or false on error
2793	 */
2794	public function parseUrl($manager, $request, $pathInfo, $rawPathInfo)
2795	{
2796		$case = 'i';	# 'i' = insensitive
2797
2798		if ($this->urlSuffix !== null)	$pathInfo = $manager->removeUrlSuffix($rawPathInfo, $this->urlSuffix);
2799
2800		$pathInfo = rtrim($pathInfo, '/').'/';
2801		// pathInfo is decoded, pattern could be encoded - required for proper url assemble (e.g. cyrillic chars)
2802		if (preg_match(rawurldecode($this->pattern).$case, $pathInfo, $matches))
2803		{
2804			foreach ($this->defaultParams as $name => $value)
2805			{
2806				//if (!isset($_GET[$name])) $_REQUEST[$name] = $_GET[$name] = $value;
2807				if (!$request->isRequestParam($name)) $request->setRequestParam($name, $value);
2808			}
2809			$tr = array();
2810			foreach ($matches as $key => $value)
2811			{
2812				if (isset($this->references[$key])) $tr[$this->references[$key]] = $value;
2813				elseif (isset($this->params[$key]))
2814				{
2815					//$_REQUEST[$key] = $_GET[$key] = $value;
2816					$request->setRequestParam($key, $value);
2817				}
2818			}
2819
2820			if ($pathInfo !== $matches[0])	# Additional GET params exist
2821			{
2822				$manager->parsePathInfo($request, ltrim(substr($pathInfo, strlen($matches[0])), '/'));
2823			}
2824			return (null !== $this->routePattern ? strtr($this->route, $tr) : $this->route);
2825		}
2826		else return false;
2827	}
2828
2829}
2830
2831abstract class eUrlConfig
2832{
2833	/**
2834	 * Registered by parse method legacy query string
2835	 */
2836	public $legacyQueryString = null;
2837
2838	/**
2839	 * User defined initialization
2840	 */
2841	public function init() {}
2842
2843	/**
2844	 * Retrieve module config options (including url rules if any)
2845	 * Return array is called once and cached, so runtime changes are not an option
2846	 * @return array
2847	 */
2848	abstract public function config();
2849
2850	/**
2851	 * Create URL callback, called only when config option selfParse is set to true
2852	 * Expected return array format:
2853	 * <code>
2854	 * array(
2855	 * 	array(part1, part2, part3),
2856	 * 	array(parm1 => val1, parm2 => val2),
2857	 * );
2858	 * </code>
2859	 * @param array $route parts
2860	 * @param array $params
2861	 * @return array|string numerical of type (routeParts, GET Params)| string route or false on error
2862	 */
2863	public function create($route, $params = array(), $options = array()) {}
2864
2865	/**
2866	 * Parse URL callback, called only when config option selfCreate is set to true
2867	 * TODO - register variable eURLConfig::currentConfig while initializing the object, remove from method arguments
2868	 * @param string $pathInfo
2869	 * @param array $params request parameters
2870	 * @param eRequest $request
2871	 * @param eRouter $router
2872	 * @param array $config
2873	 * @return string route or false on error
2874	 */
2875	public function parse($pathInfo, $params = array(), eRequest $request = null, eRouter $router = null, $config = array())
2876	{
2877		return false;
2878	}
2879
2880	/**
2881	 * Legacy callback, used called when config option legacy is not empty
2882	 * By default it sets legacy query string to $legacyQueryString value (normaly assigned inside of the parse method)
2883	 * @param string $resolvedRoute
2884	 * @param eRequest $request
2885	 * @param string $callType 'route' - called once, when parsing the request, 'dispatch' - called inside the dispatch loop (in case of controller _forward)
2886	 * @param void
2887	 */
2888	public function legacy($resolvedRoute, eRequest $request, $callType = 'route')
2889	{
2890		if($this->legacyQueryString !== null)
2891		{
2892			$request->setLegacyQstring($this->legacyQueryString);
2893			$request->setLegacyPage();
2894		}
2895	}
2896
2897	/**
2898	 * Developed mainly for legacy modules.
2899	 * It should be manually triggered inside of old entry point. The idea is
2900	 * to avoid multiple URL addresses having same content (bad SEO practice)
2901	 * FIXME - under construction
2902	 */
2903	public function forward() {}
2904
2905	/**
2906	 * Admin interface callback, returns array with all required from administration data
2907	 * Return array structure:
2908	 * <code>
2909	 * <?php
2910	 * return array(
2911	 *   'labels' => array(
2912	 *   	'name' => 'Module name',
2913	 * 		'label' => 'Profile Label',
2914	 * 		'description' => 'Additional profile info, exmples etc.',
2915	 * 	 ),
2916	 * 	 'form' => array(), // awaiting future development
2917	 * 	 'callbacks' => array(), // awaiting future development
2918	 * );
2919	 * </code>
2920	 */
2921	public function admin() { return array(); }
2922
2923	/**
2924	 * Admin submit hook
2925	 * FIXME - under construction
2926	 */
2927	public function submit() {}
2928
2929	/**
2930	 * Admin interface help messages, labels and titles
2931	 * FIXME - under construction
2932	 */
2933	public function help() {}
2934
2935
2936}
2937
2938/**
2939 * Controller base class, actions are extending it
2940 *
2941 */
2942class eController
2943{
2944	protected $_request;
2945	protected $_response;
2946
2947	public function __construct(eRequest $request, eResponse $response = null)
2948	{
2949		$this->setRequest($request)
2950			->setResponse($response)
2951			->init();
2952	}
2953
2954	/**
2955	 * Custom init, always called in the constructor, no matter what is the request dispatch status
2956	 */
2957	public function init() {}
2958
2959	/**
2960	 * Custom shutdown, always called after the controller dispatch, no matter what is the request dispatch status
2961	 */
2962	public function shutdown() {}
2963
2964	/**
2965	 * Pre-action callback, fired only if dispatch status is still true and action method is found
2966	 */
2967	public function preAction() {}
2968
2969	/**
2970	 * Post-action callback, fired only if dispatch status is still true and action method is found
2971	 */
2972	public function postAction() {}
2973
2974	/**
2975	 * @param eRequest $request
2976	 * @return eController
2977	 */
2978	public function setRequest($request)
2979	{
2980		$this->_request = $request;
2981		return $this;
2982	}
2983
2984	/**
2985	 * @return eRequest
2986	 */
2987	public function getRequest()
2988	{
2989		return $this->_request;
2990	}
2991
2992	/**
2993	 * @param eResponse $response
2994	 * @return eController
2995	 */
2996	public function setResponse($response)
2997	{
2998		$this->_response = $response;
2999		return $this;
3000	}
3001
3002	/**
3003	 * @return eResponse
3004	 */
3005	public function getResponse()
3006	{
3007		return $this->_response;
3008	}
3009
3010	public function addBody($content)
3011	{
3012		$this->getResponse()->appendBody($content);
3013		return $this;
3014	}
3015
3016	public function addMetaDescription($description)
3017	{
3018		$this->getResponse()->addMetaDescription($description);
3019		return $this;
3020	}
3021
3022	/**
3023	 * Add document title
3024	 * @param string $title
3025	 * @param boolean $meta auto-add it as meta-title
3026	 * @return eResponse
3027	 */
3028	public function addTitle($title, $meta = true)
3029	{
3030		$this->getResponse()->appendTitle($title);
3031		if($meta) $this->addMetaTitle(strip_tags($title));
3032		return $this;
3033	}
3034
3035
3036	public function addMetaTitle($title)
3037	{
3038		$this->getResponse()->addMetaTitle($title);
3039		return $this;
3040	}
3041
3042	public function dispatch($actionMethodName)
3043	{
3044		$request = $this->getRequest();
3045		$content = '';
3046
3047		// init() could modify the dispatch status
3048		if($request->isDispatched())
3049		{
3050			if(method_exists($this, $actionMethodName))
3051			{
3052				$this->preAction();
3053				// TODO request userParams() to store private data - check for noPopulate param here
3054				if($request->isDispatched())
3055				{
3056					$request->populateRequestParams();
3057
3058					// allow return output
3059					$content = $this->$actionMethodName();
3060					if(!empty($content)) $this->addBody($content);
3061
3062					if($request->isDispatched())
3063					{
3064						$this->postAction();
3065					}
3066				}
3067			}
3068			else
3069			{
3070				//TODO not found method by controller or default one
3071				$action = substr($actionMethodName, 6);
3072				throw new eException('Action "'.$action.'" does not exist');
3073			}
3074		}
3075		$this->shutdown();
3076	}
3077
3078	public function run(eRequest $request = null, eResponse $response = null)
3079	{
3080		if(null === $request) $request = $this->getRequest();
3081		else $this->setRequest($request);
3082
3083		if(null === $response) $response = $this->getResponse();
3084		else $this->setResponse($response);
3085
3086		$action = $request->getActionMethodName();
3087
3088		$request->setDispatched(true);
3089		$this->dispatch($action);
3090
3091		return $this->getResponse();
3092	}
3093
3094	protected function _redirect($url, $createURL = false, $code = null)
3095	{
3096		$redirect = e107::getRedirect();
3097		if($createURL)
3098		{
3099			$url = eFront::instance()->getRouter()->assemble($url, '', 'encode=0');
3100		}
3101		if(strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0)
3102		{
3103			$url = $url[0] == '/' ? SITEURLBASE.$url : SITEURL.$url;
3104		}
3105		$redirect->redirect($url, true, $code);
3106	}
3107
3108	/**
3109	 * System forward
3110	 * @param string $route
3111	 * @param array $params
3112	 */
3113	protected function _forward($route, $params = array())
3114	{
3115		$request = $this->getRequest();
3116
3117		if(is_string($params))
3118		{
3119			parse_str($params, $params);
3120		}
3121
3122		$oldRoute = $request->getRoute();
3123		$route = explode('/', trim($route, '/'));
3124
3125		switch (count($route)) {
3126			case 3:
3127				if($route[0] !== '*') $request->setModule($route[0]);
3128				if($route[1] !== '*') $request->setController($route[1]);
3129				$request->setAction($route[2]);
3130			break;
3131
3132			case 2:
3133				if($route[1] !== '*') $request->setController($route[0]);
3134				$request->setAction($route[1]);
3135			break;
3136
3137			case 1:
3138				$request->setAction($route[0]);
3139			break;
3140
3141			default:
3142				return;
3143			break;
3144		}
3145
3146		$request->addRouteHistory($oldRoute);
3147
3148		if(false !== $params) $request->setRequestParams($params);
3149		$request->setDispatched(false);
3150	}
3151
3152    /**
3153     * @param  string $methodName
3154     * @param  array $args
3155     * @return void
3156     * @throws eException
3157     */
3158    public function __call($methodName, $args)
3159    {
3160        if ('action' == substr($methodName, 0, 6))
3161        {
3162            $action = substr($methodName, 6);
3163            throw new eException('Action "'.$action.'" does not exist', 2404);
3164        }
3165
3166        throw new eException('Method "'.$methodName.'" does not exist', 3404);
3167    }
3168}
3169
3170/**
3171 * @package e107
3172 * @subpackage e107_handlers
3173 * @version $Id$
3174 *
3175 * Base front-end controller
3176 */
3177
3178class eControllerFront extends eController
3179{
3180	/**
3181	 * Plugin name - used to check if plugin is installed
3182	 * Set this only if plugin requires installation
3183	 * @var string
3184	 */
3185	protected $plugin = null;
3186
3187	/**
3188	 * Default controller access
3189	 * @var integer
3190	 */
3191	protected $userclass = e_UC_PUBLIC;
3192
3193	/**
3194	 * Generic 404 page URL (redirect), SITEURL will be added
3195	 * @var string
3196	 */
3197	protected $e404 = '404.html';
3198
3199	/**
3200	 * Generic 403 page URL (redirect), SITEURL will be added
3201	 * @var string
3202	 */
3203	protected $e403 = '403.html';
3204
3205	/**
3206	 * Generic 404 route URL (forward)
3207	 * @var string
3208	 */
3209	protected $e404route = 'index/not-found';
3210
3211	/**
3212	 * Generic 403 route URL (forward)
3213	 * @var string
3214	 */
3215	protected $e403route = 'index/access-denied';
3216
3217	/**
3218	 * View renderer objects
3219	 * @var array
3220	 */
3221	protected $_validator;
3222
3223	/**
3224	 * Per action access
3225	 * Format 'action' => userclass
3226	 * @var array
3227	 */
3228	protected $access = array();
3229
3230	/**
3231	 * User input filter (_GET)
3232	 * Format 'action' => array(var => validationArray)
3233	 * @var array
3234	 */
3235	protected $filter = array();
3236
3237	/**
3238	 * Base constructor - set 404/403 locations
3239	 */
3240	public function __construct(eRequest $request, eResponse $response = null)
3241	{
3242		parent::__construct($request, $response);
3243		$this->_init();
3244	}
3245
3246	/**
3247	 * Base init, called after the public init() - handle access restrictions
3248	 * The base init() method is able to change controller variables on the fly (e.g. access, filters, etc)
3249	 */
3250	final protected function _init()
3251	{
3252		// plugin check
3253		if(null !== $this->plugin)
3254		{
3255			if(!e107::isInstalled($this->plugin))
3256			{
3257				$this->forward403();
3258				return;
3259			}
3260		}
3261
3262		// global controller restriction
3263		if(!e107::getUser()->checkClass($this->userclass, false))
3264		{
3265			$this->forward403();
3266			return;
3267		}
3268
3269		// by action access
3270		if(!$this->checkActionPermissions()) exit;
3271
3272		// _GET input validation
3273		$this->validateInput();
3274
3275		// Set Render mode to module-controller-action, override possible within the action
3276		$this->getResponse()->setRenderMod(str_replace('/', '-', $this->getRequest()->getRoute()));
3277	}
3278
3279	/**
3280	 * Check persmission for current action
3281	 * @return boolean
3282	 */
3283	protected function checkActionPermissions()
3284	{
3285		// per action restrictions
3286		$action = $this->getRequest()->getAction();
3287		if(isset($this->access[$action]) && !e107::getUser()->checkClass($this->access[$action], false))
3288		{
3289			$this->forward403();
3290			return false;
3291		}
3292		return true;
3293	}
3294
3295	public function redirect404()
3296	{
3297		e107::getRedirect()->redirect(SITEURL.$this->e404);
3298	}
3299
3300	public function redirect403()
3301	{
3302		e107::getRedirect()->redirect(SITEURL.$this->e403);
3303	}
3304
3305	public function forward404()
3306	{
3307		$this->_forward($this->e404route);
3308	}
3309
3310	public function forward403()
3311	{
3312		$this->_forward($this->e403route);
3313	}
3314
3315	/**
3316	 * Controller validator object
3317	 * @return e_validator
3318	 */
3319	public function getValidator()
3320	{
3321		if(null === $this->_validator)
3322		{
3323			$this->_validator = new e_validator('controller');
3324		}
3325
3326		return $this->_validator;
3327	}
3328
3329	/**
3330	 * Register request parameters based on current $filter data (_GET only)
3331	 * Additional security layer
3332	 */
3333	public function validateInput()
3334	{
3335		$validator = $this->getValidator();
3336		$request = $this->getRequest();
3337		if(empty($this->filter) || !isset($this->filter[$request->getAction()])) return;
3338		$validator->setRules($this->filter[$request->getAction()])
3339			->validate($_GET);
3340
3341		$validData = $validator->getValidData();
3342
3343		foreach ($validData as $key => $value)
3344		{
3345			if(!$request->isRequestParam($key)) $request->setRequestParam($key, $value);
3346		}
3347		$validator->clearValidateMessages();
3348	}
3349
3350	/**
3351	 * System error message proxy
3352	 * @param string $message
3353	 * @param boolean $session
3354	 */
3355	public function messageError($message, $session = false)
3356	{
3357		return e107::getMessage()->addError($message, 'default', $session);
3358	}
3359
3360	/**
3361	 * System success message proxy
3362	 * @param string $message
3363	 * @param boolean $session
3364	 */
3365	public function messageSuccess($message, $session = false)
3366	{
3367		return e107::getMessage()->addSuccess($message, 'default', $session);
3368	}
3369
3370	/**
3371	 * System warning message proxy
3372	 * @param string $message
3373	 * @param boolean $session
3374	 */
3375	public function messageWarning($message, $session = false)
3376	{
3377		return e107::getMessage()->addWarning($message, 'default', $session);
3378	}
3379
3380	/**
3381	 * System debug message proxy
3382	 * @param string $message
3383	 * @param boolean $session
3384	 */
3385	public function messageDebug($message, $session = false)
3386	{
3387		return e107::getMessage()->addDebug($message, 'default', $session);
3388	}
3389}
3390
3391
3392/**
3393 * Request handler
3394 *
3395 */
3396class eRequest
3397{
3398	/**
3399	 * @var string
3400	 */
3401	protected $_module;
3402
3403	/**
3404	 * @var string
3405	 */
3406	protected $_controller;
3407
3408	/**
3409	 * @var string
3410	 */
3411	protected $_action;
3412
3413	/**
3414	 * Request status
3415	 * @var boolean
3416	 */
3417	protected $_dispatched = false;
3418
3419	/**
3420	 * @var array
3421	 */
3422	protected $_requestParams = array();
3423
3424	/**
3425	 * @var string
3426	 */
3427	protected $_basePath;
3428
3429	/**
3430	 * @var string
3431	 */
3432	protected $_pathInfo;
3433
3434
3435	/**
3436	 * @var string
3437	 */
3438	protected $_requestInfo;
3439
3440	/**
3441	 * Pathinfo string used for initial system routing
3442	 */
3443	public $routePathInfo;
3444
3445	/**
3446	 * @var array
3447	 */
3448	protected $_routeHistory = array();
3449
3450	/**
3451	 * @var boolean if request is already routed - generally set by callbacks to notify router about route changes
3452	 */
3453	public $routed = false;
3454
3455	/**
3456	 * Name of the bootstrap file
3457	 * @var string
3458	 */
3459	public $singleEntry = 'index.php';
3460
3461	/**
3462	 * Request constructor
3463	 */
3464	public function __construct($route = null)
3465	{
3466		if(null !== $route)
3467		{
3468			$this->setRoute($route);
3469			$this->routed = true;
3470		}
3471	}
3472
3473	/**
3474	 * Get system base path
3475	 * @return string
3476	 */
3477	public function getBasePath()
3478	{
3479		if(null == $this->_basePath)
3480		{
3481			$this->_basePath = e_HTTP;
3482			if(!e107::getPref('url_disable_pathinfo')) $this->_basePath .= $this->singleEntry.'/';
3483		}
3484
3485		return $this->_basePath;
3486	}
3487
3488	/**
3489	 * Set system base path
3490	 * @param string $basePath
3491	 * @return eRequest
3492	 */
3493	public function setBasePath($basePath)
3494	{
3495		$this->_basePath = $basePath;
3496		return $this;
3497	}
3498
3499	/**
3500	 * Get path info
3501	 * If not set, it'll be auto-retrieved
3502	 * @return string path info
3503	 */
3504	public function getPathInfo()
3505	{
3506		if(null == $this->_pathInfo)
3507		{
3508			if($this->getBasePath() == $this->getRequestInfo())
3509				$this->_pathInfo = ''; // map to indexRoute
3510
3511			else
3512				$this->_pathInfo = substr($this->getRequestInfo(), strlen($this->getBasePath()));
3513
3514			if($this->_pathInfo && trim($this->_pathInfo, '/') == trim($this->singleEntry, '/')) $this->_pathInfo = '';
3515		}
3516
3517		return $this->_pathInfo;
3518	}
3519
3520	/**
3521	 * Override path info
3522	 * @param string $pathInfo
3523	 * @return eRequest
3524	 */
3525	public function setPathInfo($pathInfo)
3526	{
3527		$this->_pathInfo = $pathInfo;
3528		return $this;
3529	}
3530
3531	/**
3532	 * @return string request info
3533	 */
3534	public function getRequestInfo()
3535	{
3536		if(null === $this->_requestInfo)
3537		{
3538			$this->_requestInfo = e_REQUEST_HTTP;
3539		}
3540		return $this->_requestInfo;
3541	}
3542
3543
3544	/**
3545	 * Override request info
3546	 * @param string $pathInfo
3547	 * @return eRequest
3548	 */
3549	public function setRequestInfo($requestInfo)
3550	{
3551		$this->_requestInfo = $requestInfo;
3552		return $this;
3553	}
3554
3555	/**
3556	 * Quick front page check
3557	 */
3558	public static function isFrontPage($entryScript = 'index.php', $currentPathInfo = e_REQUEST_HTTP)
3559	{
3560		$basePath = e_HTTP;
3561		if(!e107::getPref('url_disable_pathinfo')) $basePath .= $entryScript.'/';
3562
3563		return ($basePath == $currentPathInfo);
3564	}
3565
3566	/**
3567	 * Get current controller string
3568	 * @return string
3569	 */
3570	public function getController()
3571	{
3572		return $this->_controller;
3573	}
3574
3575	/**
3576	 * Get current controller name
3577	 * Example: requested controller-name or 'controller name' -> converted to controller_name
3578	 * @return string
3579	 */
3580	public function getControllerName()
3581	{
3582		return eHelper::underscore($this->_controller);
3583	}
3584
3585	/**
3586	 * Set current controller name
3587	 * Example: controller_name OR 'controller name' -> converted to controller-name
3588	 * Always sanitized
3589	 * @param string $controller
3590	 * @return eRequest
3591	 */
3592	public function setController($controller)
3593	{
3594		$this->_controller = strtolower(eHelper::dasherize($this->sanitize($controller)));
3595		return $this;
3596	}
3597
3598	/**
3599	 * Get current module string
3600	 * @return string
3601	 */
3602	public function getModule()
3603	{
3604		return $this->_module;
3605	}
3606
3607	/**
3608	 * Get current module name
3609	 * Example: module-name OR 'module name' -> converted to module_name
3610	 * @return string
3611	 */
3612	public function getModuleName()
3613	{
3614		return eHelper::underscore($this->_module);
3615	}
3616
3617	/**
3618	 * Set current module name
3619	 * Example: module_name OR 'module name' -> converted to module-name
3620	 * Always sanitized
3621	 * @param string $module
3622	 * @return eRequest
3623	 */
3624	public function setModule($module)
3625	{
3626		$this->_module = strtolower(eHelper::dasherize($this->sanitize($module)));
3627		return $this;
3628	}
3629
3630	/**
3631	 * Get current action string
3632	 * @return string
3633	 */
3634	public function getAction()
3635	{
3636		return $this->_action;
3637	}
3638
3639	/**
3640	 * Get current action name
3641	 * Example: action-name OR 'action name' OR action_name -> converted to ActionName
3642	 * @return string
3643	 */
3644	public function getActionName()
3645	{
3646		return eHelper::camelize($this->_action, true);
3647	}
3648
3649	/**
3650	 * Get current action method name
3651	 * Example: action-name OR 'action name' OR action_name -> converted to actionActionName
3652	 * @return string
3653	 */
3654	public function getActionMethodName()
3655	{
3656		return 'action'.eHelper::camelize($this->_action, true);
3657	}
3658
3659	/**
3660	 * Set current action name
3661	 * Example: action_name OR 'action name' OR Action_Name OR 'Action Name' -> converted to ation-name
3662	 * Always sanitized
3663	 * @param string $action
3664	 * @return eRequest
3665	 */
3666	public function setAction($action)
3667	{
3668		$this->_action = strtolower(eHelper::dasherize($this->sanitize($action)));
3669		return $this;
3670	}
3671
3672	/**
3673	 * Get current route string/array -> module/controller/action
3674	 * @param boolean $array
3675	 * @return string|array route
3676	 */
3677	public function getRoute($array = false)
3678	{
3679		if(!$this->getModule())
3680		{
3681			$route = array('index', 'index', 'index');
3682		}
3683		else
3684		{
3685			$route = array(
3686				$this->getModule(),
3687				$this->getController() ? $this->getController() : 'index',
3688				$this->getAction() ? $this->getAction() : 'index',
3689			);
3690		}
3691		return ($array ? $route : implode('/', $route));
3692	}
3693
3694	/**
3695	 * Set current route
3696	 * @param string $route module/controller/action
3697	 * @return eRequest
3698	 */
3699	public function setRoute($route)
3700	{
3701		if(null === $route)
3702		{
3703			$this->_module = null;
3704			$this->_controller = null;
3705			$this->_action = null;
3706		}
3707		return $this->initFromRoute($route);
3708	}
3709
3710	/**
3711	 * System routing track, used in controllers forwarder
3712	 * @param string $route
3713	 * @return eRequest
3714	 */
3715	public function addRouteHistory($route)
3716	{
3717		$this->_routeHistory[] = $route;
3718		return $this;
3719	}
3720
3721	/**
3722	 * Retrieve route from history track
3723	 * Based on $source we can retrieve
3724	 * - array of all history records
3725	 * - 'first' route record
3726	 * - 'last' route record
3727	 * - history record by its index number
3728	 * @param mixed $source
3729	 * @return string|array
3730	 */
3731	public function getRouteHistory($source = null)
3732	{
3733		if(null === $source) return $this->_routeHistory;
3734
3735		if(!$this->_routeHistory) return null;
3736		elseif('last' === $source)
3737		{
3738			return $this->_routeHistory[count($this->_routeHistory) -1];
3739		}
3740		elseif('first' === $source)
3741		{
3742			return $this->_routeHistory[0];
3743		}
3744		elseif(is_int($source))
3745		{
3746			return isset($this->_routeHistory[$source]) ? $this->_routeHistory[$source] : null;
3747		}
3748		return null;
3749	}
3750
3751	/**
3752	 * Search route history for the given $route
3753	 *
3754	 * @param string $route
3755	 * @return integer route index or false if not found
3756	 */
3757	public function findRouteHistory($route)
3758	{
3759		return array_search($route, $this->_routeHistory);
3760	}
3761
3762	/**
3763	 * Populate module, controller and action from route string
3764	 * @param string $route
3765	 * @return array route data
3766	 */
3767	public function initFromRoute($route)
3768	{
3769		$route = trim($route, '/');
3770		if(!$route)
3771		{
3772			$route = 'index/index/index';
3773		}
3774		$parts = explode('/', $route);
3775		$this->setModule($parts[0])
3776			->setController(vartrue($parts[1], 'index'))
3777			->setAction(vartrue($parts[2], 'index'));
3778
3779		return $this;//->getRoute(true);
3780	}
3781
3782	/**
3783	 * Get request parameter
3784	 * @param string $key
3785	 * @param string $default value if key not set
3786	 * @return mixed value
3787	 */
3788	public function getRequestParam($key, $default = null)
3789	{
3790		return (isset($this->_requestParams[$key]) ? $this->_requestParams[$key] : $default);
3791	}
3792
3793	/**
3794	 * Check if request parameter exists
3795	 * @param string $key
3796	 * @return boolean
3797	 */
3798	public function isRequestParam($key)
3799	{
3800		return isset($this->_requestParams[$key]);
3801	}
3802
3803	/**
3804	 * Get request parameters array
3805	 * @return array value
3806	 */
3807	public function getRequestParams()
3808	{
3809		return $this->_requestParams;
3810	}
3811
3812	/**
3813	 * Set request parameter
3814	 * @param string $key
3815	 * @param mixed $value
3816	 * @return eRequest
3817	 */
3818	public function setRequestParam($key, $value)
3819	{
3820		$this->_requestParams[$key] = $value;
3821		return $this;
3822	}
3823
3824	/**
3825	 * Set request parameters
3826	 * @param array $params
3827	 * @return eRequest
3828	 */
3829	public function setRequestParams($params)
3830	{
3831		$this->_requestParams = $params;
3832		return $this;
3833	}
3834
3835	/**
3836	 * Populate current request parameters (_GET scope)
3837	 * @return eRequest
3838	 */
3839	public function populateRequestParams()
3840	{
3841		$rp = $this->getRequestParams();
3842
3843		foreach ($rp as $key => $value)
3844		{
3845			$_GET[$key] = $value;
3846		}
3847		return $this;
3848	}
3849
3850	/**
3851	 * More BC
3852	 * @param string $qstring
3853	 * @return eRequest
3854	 */
3855	public function setLegacyQstring($qstring = null)
3856	{
3857		if(defined('e_QUERY')) return $this;
3858
3859		if(null === $qstring)
3860		{
3861			$qstring = self::getQueryString();
3862		}
3863
3864		if(!defined('e_SELF'))
3865		{
3866			define("e_SELF", e_REQUEST_SELF);
3867		}
3868
3869		if(!defined('e_QUERY'))
3870		{
3871			define("e_QUERY", $qstring);
3872		}
3873
3874		$_SERVER['QUERY_STRING'] = e_QUERY;
3875
3876		if(strpos(e_QUERY,"=")!==false ) // Fix for legacyQuery using $_GET ie. ?x=y&z=1 etc.
3877		{
3878			parse_str(str_replace(array('&amp;'), array('&'), e_QUERY),$tmp);
3879			foreach($tmp as $key=>$value)
3880			{
3881				$_GET[$key] = $value;
3882			}
3883		}
3884
3885		return $this;
3886	}
3887
3888	/**
3889	 * And More BC :/
3890	 * @param string $page
3891	 * @return eRequest
3892	 */
3893	public function setLegacyPage($page = null)
3894	{
3895		if(defined('e_PAGE')) return $this;
3896		if(null === $page)
3897		{
3898			$page = eFront::isLegacy();
3899		}
3900		if(!$page)
3901		{
3902			define('e_PAGE', $this->singleEntry);
3903		}
3904		else define('e_PAGE', basename(str_replace(array('{', '}'), '/', $page)));
3905		return $this;
3906	}
3907
3908	/**
3909	 * And More from the same - BC :/
3910	 * @return string
3911	 */
3912	public static function getQueryString()
3913	{
3914		$qstring = '';
3915		if($_SERVER['QUERY_STRING'])
3916		{
3917			$qstring = str_replace(array('{', '}', '%7B', '%7b', '%7D', '%7d'), '', rawurldecode($_SERVER['QUERY_STRING']));
3918		}
3919		$qstring = str_replace('&', '&amp;', e107::getParser()->post_toForm($qstring));
3920		return $qstring;
3921	}
3922
3923	/**
3924	 * Basic sanitize method for module, controller and action input values
3925	 * @param string $str string to be sanitized
3926	 * @param string $pattern optional replace pattern
3927	 * @param string $replace optional replace string, defaults to dash
3928	 */
3929	public function sanitize($str, $pattern='', $replace='-')
3930	{
3931		if (!$pattern) $pattern = '/[^\w\pL-]/u';
3932
3933		return preg_replace($pattern, $replace, $str);
3934	}
3935
3936	/**
3937	 * Set dispatched status of the request
3938	 * @param boolean $mod
3939	 * @return eRequest
3940	 */
3941	public function setDispatched($mod)
3942	{
3943		$this->_dispatched = $mod ? true : false;
3944		return $this;
3945	}
3946
3947	/**
3948	 * Get dispatched status of the request
3949	 * @return boolean
3950	 */
3951	public function isDispatched()
3952	{
3953		return $this->_dispatched;
3954	}
3955}
3956
3957class eResponse
3958{
3959	protected $_body = array('default' => '');
3960	protected $_title = array('default' => array());
3961	protected $_e_PAGETITLE = array();
3962	protected $_META_DESCRIPTION = array();
3963	protected $_META_KEYWORDS = array();
3964	protected $_render_mod = array('default' => 'default');
3965	protected $_meta_title_separator = ' - ';
3966	protected $_meta_name_only = array('keywords', 'viewport', 'robots'); // Keep FB happy.
3967	protected $_meta_property_only = array('article:section', 'article:tag'); // Keep FB happy.
3968	protected $_meta = array();
3969	protected $_meta_robot_types = array('noindex'=>'NoIndex', 'nofollow'=>'NoFollow','noarchive'=>'NoArchive','noimageindex'=>'NoImageIndex' );
3970	protected $_title_separator = ' &raquo; ';
3971	protected $_content_type = 'html';
3972	protected $_content_type_arr =  array(
3973		'html' => 'text/html',
3974		'css' => 'text/css',
3975		'xml' => 'text/xml',
3976		'json' => 'application/json',
3977		'js' => 'application/javascript',
3978		'rss' => 'application/rss+xml',
3979		'soap' => 'application/soap+xml',
3980	);
3981
3982	protected $_params = array(
3983		'render' => true,
3984		'meta' => false,
3985		'jsonNoTitle' => false,
3986		'jsonRender' => false,
3987	);
3988
3989	public function getRobotTypes()
3990	{
3991		return $this->_meta_robot_types;
3992	}
3993
3994	public function getRobotDescriptions()
3995	{
3996		$_meta_robot_descriptions = array(
3997			'noindex'       => LAN_ROBOTS_NOINDEX,
3998			'nofollow'      => LAN_ROBOTS_NOFOLLOW,
3999			'noarchive'     => LAN_ROBOTS_NOARCHIVE,
4000			'noimageindex'  => LAN_ROBOTS_NOIMAGE );
4001
4002		return $_meta_robot_descriptions;
4003	}
4004
4005	public function setParam($key, $value)
4006	{
4007		$this->_params[$key] = $value;
4008		return $this;
4009	}
4010
4011	public function setParams($params)
4012	{
4013		$this->_params = $params;
4014		return $this;
4015	}
4016
4017	public function getParam($key, $default = null)
4018	{
4019		return (isset($this->_params[$key]) ? $this->_params[$key] : $default);
4020	}
4021
4022	public function isParam($key)
4023	{
4024		return isset($this->_params[$key]);
4025	}
4026
4027	public function addContentType($typeName, $mediaType)
4028	{
4029		$this->_content_type_arr[$typeName] = $mediaType;
4030		return $this;
4031	}
4032
4033	public function getContentType()
4034	{
4035		return $this->_content_type;
4036	}
4037
4038	public function getContentMediaType($typeName)
4039	{
4040		if(isset($this->_content_type_arr[$typeName]))
4041			return $this->_content_type_arr[$typeName];
4042	}
4043
4044	public function setContentType($typeName)
4045	{
4046		$this->_content_type = $typeName;
4047	}
4048
4049	/**
4050	 * @return eResponse
4051	 */
4052	public function sendContentType()
4053	{
4054		$ctypeStr = $this->getContentMediaType($this->getContentType());
4055		if($ctypeStr)
4056		{
4057			header('Content-type: '.$this->getContentMediaType($this->getContentType()).'; charset=utf-8', TRUE);
4058		}
4059		return $this;
4060	}
4061
4062	/**
4063	 * @return eResponse
4064	 */
4065	public function addHeader($header, $override = false, $responseCode = null)
4066	{
4067		header($header, $override, $responseCode);
4068		return $this;
4069	}
4070
4071	/**
4072	 * Append content
4073	 * @param string $body
4074	 * @param string $ns namespace
4075	 * @return eResponse
4076	 */
4077	public function appendBody($body, $ns = 'default')
4078	{
4079		if(!isset($this->_body[$ns]))
4080		{
4081			$this->_body[$ns] = '';
4082		}
4083		$this->_body[$ns] .= $body;
4084
4085		return $this;
4086	}
4087
4088	/**
4089	 * Set content
4090	 * @param string $body
4091	 * @param string $ns namespace
4092	 * @return eResponse
4093	 */
4094	public function setBody($body, $ns = 'default')
4095	{
4096		$this->_body[$ns] = $body;
4097		return $this;
4098	}
4099
4100
4101	/**
4102	 * @param $name
4103	 * @param $content
4104	 * @return $this
4105	 */
4106	public function setMeta($name, $content)
4107	{
4108		foreach($this->_meta as $k=>$v)
4109		{
4110			if($v['name'] === $name)
4111			{
4112				$this->_meta[$k]['content'] = $content;
4113			}
4114		}
4115
4116		return $this;
4117
4118	}
4119
4120
4121	/**
4122	 * Removes a Meta tag by name/property.
4123	 *
4124	 * @param string $name
4125	 *   'name' or 'property' for the meta tag we want to remove.
4126	 *
4127	 * @return eResponse $this
4128	 */
4129	public function removeMeta($name)
4130	{
4131		foreach($this->_meta as $k=>$v)
4132		{
4133			// Meta tags like: <meta content="..." name="description" />
4134			if(isset($v['name']) && $v['name'] === $name)
4135			{
4136				unset($this->_meta[$k]);
4137				continue;
4138			}
4139
4140			// Meta tags like: <meta content="..." property="og:title" />
4141			if(isset($v['property']) && $v['property'] === $name)
4142			{
4143				unset($this->_meta[$k]);
4144			}
4145		}
4146
4147		return $this;
4148	}
4149
4150
4151	/**
4152	 * Prepend content
4153	 * @param string $body
4154	 * @param string $ns namespace
4155	 * @return eResponse
4156	 */
4157	function prependBody($body, $ns = 'default')
4158	{
4159		if(!isset($this->_body[$ns]))
4160		{
4161			$this->_body[$ns] = '';
4162		}
4163		// $this->_body[$ns] = $content.$this->_body[$ns];
4164
4165		return $this;
4166	}
4167
4168	/**
4169	 * Get content
4170	 * @param string $ns
4171	 * @param boolean $reset
4172	 * @return string
4173	 */
4174	public function getBody($ns = 'default', $reset = false)
4175	{
4176		if(!isset($this->_body[$ns]))
4177		{
4178			$this->_body[$ns] = '';
4179		}
4180		$ret = $this->_body[$ns];
4181		if($reset) unset($this->_body[$ns]);
4182
4183		return $ret;
4184	}
4185
4186	/**
4187	 * @param string $title
4188	 * @param string $ns
4189	 * @return eResponse
4190	 */
4191	function setTitle($title, $ns = 'default')
4192	{
4193
4194		if(!is_string($ns) || empty($ns))
4195		{
4196			$this->_title['default'] = array((string) $title);
4197		}
4198		else
4199		{
4200			$this->_title[$ns] = array((string) $title);
4201		}
4202		return $this;
4203	}
4204
4205	/**
4206	 * @param string $title
4207	 * @param string $ns
4208	 * @return eResponse
4209	 */
4210	function appendTitle($title, $ns = 'default')
4211	{
4212		if(empty($title))
4213		{
4214			return $this;
4215		}
4216		if(!is_string($ns) || empty($ns))
4217		{
4218			$ns = 'default';
4219		}
4220		elseif(!isset($this->_title[$ns]))
4221		{
4222			$this->_title[$ns] = array();
4223		}
4224		$this->_title[$ns][] = (string) $title;
4225		return $this;
4226	}
4227
4228	/**
4229	 * @param string $title
4230	 * @param string $ns
4231	 * @return eResponse
4232	 */
4233	function prependTitle($title, $ns = 'default')
4234	{
4235		if(empty($title))
4236		{
4237			return $this;
4238		}
4239		if(!is_string($ns) || empty($ns))
4240		{
4241			$ns = 'default';
4242		}
4243		elseif(!isset($this->_title[$ns]))
4244		{
4245			$this->_title[$ns] = array();
4246		}
4247		array_unshift($this->_title[$ns], $title);
4248		return $this;
4249	}
4250
4251	/**
4252	 * Assemble title
4253	 * @param string $ns
4254	 * @param bool $reset
4255	 * @return string
4256	 */
4257	function getTitle($ns = 'default', $reset = false)
4258	{
4259		if(!is_string($ns) || empty($ns))
4260		{
4261			$ret = implode($this->_title_separator, $this->_title['default']);
4262			if($reset)
4263				$this->_title['default'] = '';
4264		}
4265		elseif(isset($this->_title[$ns]))
4266		{
4267			$ret = implode($this->_title_separator, $this->_title[$ns]);
4268			if($reset)
4269				unset($this->_title[$ns]);
4270		}
4271		else
4272		{
4273			$ret = '';
4274		}
4275		return $ret;
4276	}
4277
4278	/**
4279	 *
4280	 * @param string $render_mod
4281	 * @param mixed $ns
4282	 * @return eResponse
4283	 */
4284	function setRenderMod($render_mod, $ns = 'default')
4285	{
4286		$this->_render_mod[$ns] = $render_mod;
4287		return $this;
4288	}
4289
4290	/**
4291	 * Retrieve render mod
4292	 * @param mixed $ns
4293     * @return mixed
4294	 */
4295	function getRenderMod($ns = 'default')
4296	{
4297		if(!is_string($ns) || empty($ns))
4298		{
4299			$ns = 'default';
4300		}
4301		return vartrue($this->_render_mod[$ns], null);
4302	}
4303
4304	/**
4305	 * Generic meta information
4306	 * Example usage:
4307	 * addMeta('og:title', 'My Title');
4308	 * addMeta(null, 30, array('http-equiv' => 'refresh'));
4309	 * addMeta(null, null, array('http-equiv' => 'refresh', 'content' => 30)); // same as above
4310	 * @param string $name 'name' attribute value, or null to avoid it
4311	 * @param string $content 'content' attribute value, or null to avoid it
4312	 * @param array $extended format 'attribute_name' => 'value'
4313	 * @return eResponse
4314	 */
4315	public function addMeta($name = null, $content = null, $extended = array())
4316	{
4317		if(empty($content)){ return $this; } // content is required, otherwise ignore.
4318
4319		//TODO need an option that allows subsequent entries to overwrite existing ones.
4320		//ie. 'description' and 'keywords' should never be duplicated, but overwritten by plugins and other non-pref-based meta data.
4321
4322
4323
4324
4325		$attr = array();
4326
4327		if(null !== $name)
4328		{
4329		//	$key = (substr($name,0,3) == 'og:') ? 'property' : 'name';
4330		//	$attr[$key] = $name;
4331			if(!in_array($name, $this->_meta_name_only))
4332			{
4333				$attr['property'] = $name;  // giving both should be valid and avoid issues with FB and others.
4334			}
4335
4336			if(!in_array($name, $this->_meta_property_only))
4337			{
4338				$attr['name'] = $name;
4339			}
4340		}
4341
4342
4343
4344		if(null !== $content) $attr['content'] = $content;
4345		if(!empty($extended))
4346		{
4347			if(!empty($attr))  $attr = array_merge($attr, $extended);
4348			else $attr = $extended;
4349		}
4350
4351
4352		if(!empty($attr))
4353		{
4354			if($name === 'keywords') // prevent multiple keyword tags.
4355			{
4356			    $this->_meta['keywords'] = $attr;
4357			}
4358			else
4359			{
4360				$this->_meta[] = $attr;
4361			}
4362		}
4363
4364		return $this;
4365	}
4366
4367	/**
4368	 * Render meta tags, registered via addMeta() method
4369	 * @return string
4370	 */
4371	public function renderMeta()
4372	{
4373		$attrData = '';
4374
4375		e107::getEvent()->trigger('system_meta_pre', $this->_meta);
4376
4377		$pref = e107::getPref();
4378
4379		if(!empty($pref['meta_keywords'][e_LANGUAGE])) // Always append (global) meta keywords to the end.
4380		{
4381			$tmp1 = (array) explode(",", $this->getMetaKeywords());
4382			$tmp2 = (array) explode(",", $pref['meta_keywords'][e_LANGUAGE]);
4383
4384			$tmp3 = array_unique(array_merge($tmp1,$tmp2));
4385
4386			$this->setMeta('keywords', implode(',',$tmp3));
4387		}
4388
4389
4390
4391		e107::getDebug()->log($this->_meta);
4392
4393
4394		foreach ($this->_meta as $attr)
4395		{
4396			$attrData .= '<meta';
4397			foreach ($attr as $p => $v)
4398			{
4399				$attrData .= ' '.preg_replace('/[^\w\-]/', '', $p).'="'.str_replace(array('"', '<'), '', $v).'"';
4400			}
4401			$attrData .= ' />'."\n";
4402		}
4403
4404		return $attrData;
4405	}
4406
4407	/**
4408	 * Add meta title, description and keywords
4409	 *
4410	 * @param string $meta property name
4411	 * @param string $content meta content
4412	 * @return eResponse
4413	 */
4414	function addMetaData($meta, $content)
4415	{
4416		$meta = '_' . $meta;
4417		if(isset($this->$meta) && !empty($content))
4418		{
4419			$content = str_replace(array('&amp;', '"', "'"), array('&', '', ''), $content);
4420			$this->{$meta}[] = htmlspecialchars((string) $content, ENT_QUOTES, 'UTF-8');
4421		}
4422		return $this;
4423	}
4424
4425	/**
4426	 * Get meta title, description and keywords
4427	 *
4428	 * @param string $meta property name
4429	 * @return string
4430	 */
4431	function getMetaData($meta, $separator = '')
4432	{
4433		$meta = '_' . $meta;
4434		if(isset($this->$meta) && !empty($this->$meta))
4435		{
4436			return implode($separator, $this->$meta);
4437		}
4438		return '';
4439	}
4440
4441
4442
4443	/**
4444	 * Return an array of all meta data
4445	 * @return array
4446	 */
4447	function getMeta()
4448	{
4449		return $this->_meta;
4450	}
4451
4452
4453	/**
4454	 * @param string $title
4455	 * @return eResponse
4456	 */
4457	function addMetaTitle($title)
4458	{
4459		return $this->addMetaData('e_PAGETITLE', $title);
4460	}
4461
4462	function getMetaTitle()
4463	{
4464		return $this->getMetaData('e_PAGETITLE', $this->_meta_title_separator);
4465	}
4466
4467	/**
4468	 * @param string $description
4469	 * @return eResponse
4470	 */
4471	function addMetaDescription($description)
4472	{
4473		return $this->addMetaData('META_DESCRIPTION', $description);
4474	}
4475
4476	function getMetaDescription()
4477	{
4478		return $this->getMetaData('META_DESCRIPTION');
4479	}
4480
4481	/**
4482	 * @param string $keywords
4483	 * @return eResponse
4484	 */
4485	function addMetaKeywords($keywords)
4486	{
4487		return $this->addMetaData('META_KEYWORDS', $keywords);
4488	}
4489
4490	function getMetaKeywords()
4491	{
4492		return $this->getMetaData('META_KEYWORDS', ',');
4493	}
4494
4495	/**
4496	 * Send e107 meta-data
4497	 * @return eResponse
4498	 */
4499	function sendMeta()
4500	{
4501		//HEADERF already included or meta content already sent
4502		if(e_AJAX_REQUEST || defined('USER_AREA') || defined('e_PAGETITLE'))
4503			return $this;
4504
4505		if(!defined('e_PAGETITLE') && !empty($this->_e_PAGETITLE))
4506		{
4507			define('e_PAGETITLE', $this->getMetaTitle());
4508		}
4509		if(!defined('META_DESCRIPTION') && !empty($this->_META_DESCRIPTION))
4510		{
4511			define('META_DESCRIPTION', $this->getMetaDescription());
4512		}
4513		if(!defined('META_KEYWORDS') && !empty($this->_META_KEYWORDS))
4514		{
4515			define('META_KEYWORDS', $this->getMetaKeywords());
4516		}
4517		return $this;
4518	}
4519
4520	/**
4521	 * Send Response Output - default method
4522	 * TODO - ajax send, using js_manager
4523	 * @param string $ns namespace/segment
4524	 * @param bool $return
4525	 * @param bool $render_message append system messages
4526	 * @return null|string
4527	 */
4528	function send($ns = null, $return = true, $render_message = true)
4529	{
4530		$content = $this->getBody($ns, true);
4531		$render = $this->getParam('render');
4532		$meta = $this->getParam('meta');
4533
4534		$this->sendContentType();
4535
4536		if($render_message)
4537		{
4538			$content = eMessage::getInstance()->render().$content;
4539		}
4540
4541		if($meta)
4542		{
4543			$this->sendMeta();
4544		}
4545
4546		//render disabled by the controller
4547		if(!$this->getRenderMod($ns))
4548		{
4549			$render = false;
4550		}
4551
4552		if($render)
4553		{
4554			$render = e107::getRender();
4555			if($return)
4556			{
4557				return $render->tablerender($this->getTitle($ns, true), $content, $this->getRenderMod($ns), true);
4558			}
4559			else
4560			{
4561				$render->tablerender($this->getTitle($ns, true), $content, $this->getRenderMod($ns));
4562				return '';
4563			}
4564		}
4565		elseif($return)
4566		{
4567			return $content;
4568		}
4569		else
4570		{
4571			print $content;
4572			return '';
4573		}
4574	}
4575
4576	/**
4577	 * Send AJAX Json Response Output - default method
4578	 * It's fully compatible with the core dialog.js
4579	 * @param array $override override output associative array (header, body and footer keys)
4580	 * @param string $ns namespace/segment
4581	 * @param bool $render_message append system messages
4582	 */
4583	function sendJson($override = array(), $ns = null, $render_message = true)
4584	{
4585		if(!$ns) $ns = 'default';
4586
4587		$content = $this->getBody($ns, true);
4588		// separate render parameter for json response, false by default
4589		$render = $this->getParam('jsonRender');
4590		if($render_message)
4591		{
4592			$content = eMessage::getInstance()->render().$content;
4593		}
4594
4595		//render disabled by the controller
4596		if(!$this->getRenderMod($ns))
4597		{
4598			$render = false;
4599		}
4600
4601
4602		$title = '';
4603		if(!$this->getParam('jsonNoTitle'))
4604		{
4605			$titleArray = $this->_title;
4606			$title = isset($titleArray[$ns]) ? array_pop($titleArray[$ns]) : '';
4607		}
4608
4609		if($render)
4610		{
4611			$render = e107::getRender();
4612			$content = $render->tablerender($this->getTitle($ns, true), $content, $this->getRenderMod($ns), true);
4613		}
4614
4615		$jshelper = e107::getJshelper();
4616		$override = array_merge(array(
4617			'header' => $title,
4618			'body' => $content,
4619		//	'footer' => $statusText, // FIXME $statusText has no value.
4620		), $override);
4621		echo $jshelper->buildJsonResponse($override);
4622		$jshelper->sendJsonResponse(null);
4623	}
4624
4625	/**
4626	 * JS manager
4627	 * @return e_jsmanager
4628	 */
4629	function getJs()
4630	{
4631		return e107::getJs();
4632	}
4633}
4634
4635/**
4636 * We move all generic helper functionallity here - a lot of candidates in e107 class
4637 *
4638 */
4639class eHelper
4640{
4641	protected static $_classRegEx = '#[^\w\s\-]#';
4642	protected static $_idRegEx = '#[^\w\-]#';
4643	protected static $_styleRegEx = '#[^\w\s\-\.;:!]#';
4644
4645	public static function secureClassAttr($string)
4646	{
4647		return preg_replace(self::$_classRegEx, '', $string);
4648	}
4649
4650	public static function secureIdAttr($string)
4651	{
4652		$string = str_replace(array('/','_'),'-',$string);
4653		return preg_replace(self::$_idRegEx, '', $string);
4654	}
4655
4656	public static function secureStyleAttr($string)
4657	{
4658		return preg_replace(self::$_styleRegEx, '', $string);
4659	}
4660
4661	public static function buildAttr($safeArray)
4662	{
4663		return http_build_query($safeArray, null, '&');
4664	}
4665
4666	public static function formatMetaTitle($title)
4667	{
4668		$title = trim(str_replace(array('"', "'"), '', strip_tags(e107::getParser()->toHTML($title, TRUE))));
4669		return trim(preg_replace('/[\s,]+/', ' ', str_replace('_', ' ', $title)));
4670	}
4671
4672	public static function secureSef($sef)
4673	{
4674		return trim(preg_replace('/[^\w\pL\s\-+.,]+/u', '', strip_tags(e107::getParser()->toHTML($sef, TRUE))));
4675	}
4676
4677	public static function formatMetaKeys($keywordString)
4678	{
4679		$keywordString = preg_replace('/[^\w\pL\s\-.,+]/u', '', strip_tags(e107::getParser()->toHTML($keywordString, TRUE)));
4680		return trim(preg_replace('/[\s]?,[\s]?/', ',', str_replace('_', ' ', $keywordString)));
4681	}
4682
4683	public static function formatMetaDescription($descrString)
4684	{
4685		$descrString = preg_replace('/[\r]*\n[\r]*/', ' ', trim(str_replace(array('"', "'"), '', strip_tags(e107::getParser()->toHTML($descrString, TRUE)))));
4686		return trim(preg_replace('/[\s]+/', ' ', str_replace('_', ' ', $descrString)));
4687	}
4688
4689	/**
4690	 * Convert title to valid SEF URL string
4691	 * Type ending with 'l' stands for 'to lowercase', ending with 'c' - 'to camel case'
4692	 * @param string $title
4693	 * @param string $type dashl|dashc|dash|underscorel|underscorec|underscore|plusl|plusc|plus|none
4694	 * @return mixed|string
4695	 */
4696	public static function title2sef($title, $type = null)
4697	{
4698		/*$char_map = array(
4699			// Latin
4700			'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C',
4701			'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
4702			'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O',
4703			'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
4704			'ß' => 'ss',
4705			'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c',
4706			'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
4707			'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o',
4708			'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th',
4709			'ÿ' => 'y',
4710			// Latin symbols
4711			'©' => '(c)',
4712			// Greek
4713			'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8',
4714			'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P',
4715			'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W',
4716			'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I',
4717			'Ϋ' => 'Y',
4718			'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8',
4719			'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p',
4720			'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w',
4721			'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's',
4722			'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i',
4723			// Turkish
4724			'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G',
4725			'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g',
4726			// Russian
4727			'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh',
4728			'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O',
4729			'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
4730			'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu',
4731			'Я' => 'Ya',
4732			'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh',
4733			'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o',
4734			'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c',
4735			'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu',
4736			'я' => 'ya',
4737			// Ukrainian
4738			'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G',
4739			'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g',
4740			// Czech
4741			'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U',
4742			'Ž' => 'Z',
4743			'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u',
4744			'ž' => 'z',
4745			// Polish
4746			'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z',
4747			'Ż' => 'Z',
4748			'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z',
4749			'ż' => 'z',
4750			// Latvian
4751			'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N',
4752			'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z',
4753			'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n',
4754			'š' => 's', 'ū' => 'u', 'ž' => 'z'
4755		);*/
4756
4757		$tp = e107::getParser();
4758
4759		// issue #3245: strip all html and bbcode before processing
4760		$title = $tp->toText($title);
4761
4762		$title = $tp->toASCII($title);
4763
4764		$title = str_replace(array('/',' ',","),' ',$title);
4765		$title = str_replace(array("&","(",")"),'',$title);
4766		$title = preg_replace('/[^\w\d\pL\s.-]/u', '', strip_tags(e107::getParser()->toHTML($title, TRUE)));
4767		$title = trim(preg_replace('/[\s]+/', ' ', str_replace('_', ' ', $title)));
4768		$title = str_replace(array(' - ',' -','- ','--'),'-',$title); // cleanup to avoid ---
4769
4770		$words = str_word_count($title,1, '1234567890');
4771
4772		$limited = array_slice($words, 0, 14); // Limit number of words to 14. - any more and it ain't friendly.
4773
4774		$title = implode(" ",$limited);
4775
4776		if(null === $type)
4777		{
4778			$type = e107::getPref('url_sef_translate');
4779		}
4780
4781		switch ($type)
4782		{
4783			case 'dashl': //dasherize, to lower case
4784				return self::dasherize($tp->ustrtolower($title));
4785			break;
4786
4787			case 'dashc': //dasherize, camel case
4788				return self::dasherize(self::camelize($title, true, ' '));
4789			break;
4790
4791			case 'dash': //dasherize
4792				return self::dasherize($title);
4793			break;
4794
4795			case 'underscorel': ///underscore, to lower case
4796				return self::underscore($tp->ustrtolower($title));
4797			break;
4798
4799			case 'underscorec': ///underscore, camel case
4800				return self::underscore(self::camelize($title, true, ' '));
4801			break;
4802
4803			case 'underscore': ///underscore
4804				return self::underscore($title);
4805			break;
4806
4807			case 'plusl': ///plus separator, to lower case
4808				return str_replace(' ', '+', $tp->ustrtolower($title));
4809			break;
4810
4811			case 'plusc': ///plus separator, to lower case
4812				return str_replace(' ', '+', self::camelize($title, true, ' '));
4813			break;
4814
4815			case 'plus': ///plus separator
4816				return str_replace(' ', '+', $title);
4817			break;
4818
4819			case 'none':
4820			default:
4821				return $title;
4822			break;
4823		}
4824	}
4825
4826	/**
4827	 * Return a memory value formatted helpfully
4828	 * $dp overrides the number of decimal places displayed - realistically, only 0..3 are sensible
4829	 * FIXME e107->parseMemorySize() START
4830	 * - move here all e107 class ban/ip related methods
4831	 * - out of (integer) range case?
4832	 * 32 bit systems range: -2147483648 to 2147483647
4833	 * 64 bit systems range: -9223372036854775808 9223372036854775807
4834	 * {@link http://www.php.net/intval}
4835	 * FIXME e107->parseMemorySize() END
4836	 *
4837	 * @param integer $size
4838	 * @param integer $dp
4839	 * @return string formatted size
4840	 */
4841	public static function parseMemorySize($size, $dp = 2)
4842	{
4843		if (!$size) { $size = 0; }
4844		if ($size < 4096)
4845		{	// Fairly arbitrary limit below which we always return number of bytes
4846			return number_format($size, 0).CORE_LAN_B;
4847		}
4848
4849		$size = $size / 1024;
4850		$memunit = CORE_LAN_KB;
4851
4852		if ($size > 1024)
4853		{ /* 1.002 mb, etc */
4854			$size = $size / 1024;
4855			$memunit = CORE_LAN_MB;
4856		}
4857		if ($size > 1024)
4858		{ /* show in GB if >1GB */
4859			$size = $size / 1024;
4860			$memunit = CORE_LAN_GB;
4861		}
4862		if ($size > 1024)
4863		{ /* show in TB if >1TB */
4864			$size = $size / 1024;
4865			$memunit = CORE_LAN_TB;
4866		}
4867		return (number_format($size, $dp).$memunit);
4868	}
4869
4870	/**
4871	 * Get the current memory usage of the code
4872	 * If $separator argument is null, raw data (array) will be returned
4873	 *
4874	 * @param null|string $separator
4875	 * @return string|array memory usage
4876	 */
4877	public static function getMemoryUsage($separator = '/')
4878	{
4879		$ret = array();
4880		if(function_exists("memory_get_usage"))
4881		{
4882	      $ret[] = eHelper::parseMemorySize(memory_get_usage());
4883		  // With PHP>=5.2.0, can show peak usage as well
4884	      if (function_exists("memory_get_peak_usage")) $ret[] = eHelper::parseMemorySize(memory_get_peak_usage(TRUE));
4885		}
4886		else
4887		{
4888		  $ret[] = 'Unknown';
4889		}
4890
4891		return (null !== $separator ? implode($separator, $ret) : $ret);
4892	}
4893
4894	public static function camelize($str, $all = false, $space = '')
4895	{
4896		// clever recursion o.O
4897		if($all) return self::camelize('-'.$str, false, $space);
4898
4899		$tmp = explode('-', str_replace(array('_', ' '), '-', e107::getParser()->ustrtolower($str)));
4900		return trim(implode($space, array_map('ucfirst', $tmp)), $space);
4901	}
4902
4903	public static function labelize($str, $space = ' ')
4904	{
4905		return self::camelize($str, true, ' ');
4906	}
4907
4908	public static function dasherize($str)
4909	{
4910		return str_replace(array('_', ' '), '-', $str);
4911	}
4912
4913	public static function underscore($str)
4914	{
4915		return str_replace(array('-', ' '), '_', $str);
4916	}
4917
4918	/**
4919	 * Parse generic shortcode parameter string
4920	 * Format expected: {SC=key=val&key1=val1...}
4921	 * Escape strings: \& => &
4922	 *
4923	 * @param string $parmstr
4924	 * @return array associative param array
4925	 */
4926	public static function scParams($parm)
4927	{
4928		if (!$parm) return array();
4929		if (!is_array($parm))
4930		{
4931			$parm = str_replace('\&', '%%__amp__%%', $parm);
4932			$parm = str_replace('&amp;', '&', $parm); // clean when it comes from the DB
4933			parse_str($parm, $parm);
4934			foreach ($parm as $k => $v)
4935			{
4936				$parm[str_replace('%%__amp__%%', '&', $k)] = str_replace('%%__amp__%%', '\&', $v);
4937			}
4938		}
4939
4940		return $parm;
4941	}
4942
4943	/**
4944	 * Parse shortcode parameter string of type 'dual parameters' - advanced, more complex and slower(!) case
4945	 * Format expected: {SC=name|key=val&key1=val1...}
4946	 * Escape strings: \| => | , \& => & and \&amp; => &amp;
4947	 * Return array is formatted like this:
4948	 * 1 => string|array (depends on $name2array value) containing first set of parameters;
4949	 * 2 => array containing second set of parameters;
4950	 * 3 => string containing second set of parameters;
4951	 *
4952	 * @param string $parmstr
4953	 * @param boolean $first2array If true, first key (1) of the returned array will be parsed to array as well
4954	 * @return array
4955	 */
4956	public static function scDualParams($parmstr, $first2array = false)
4957	{
4958		if (!$parmstr) return array(1 => '', 2 => array(), 3 => '');
4959		if (is_array($parmstr)) return $parmstr;
4960
4961		$parmstr = str_replace('&amp;', '&', $parmstr); // clean when it comes from the DB
4962		$parm = explode('|', str_replace(array('\|', '\&amp;', '\&'), array('%%__pipe__%%', '%%__ampamp__%%', '%%__amp__%%'), $parmstr), 2);
4963
4964		$multi = str_replace('%%__pipe__%%', '|', $parm[0]);
4965		if ($first2array)
4966		{
4967			parse_str($multi, $multi);
4968			foreach ($multi as $k => $v)
4969			{
4970				$multi[str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $k)] = str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $v);
4971			}
4972		}
4973
4974		if (varset($parm[1]))
4975		{
4976			// second paramater as a string - allow to be further passed to shortcodes
4977			$parmstr = str_replace(array('%%__pipe__%%', '%%__ampamp__%%', '%%__amp__%%'), array('\|', '\&amp;', '\&'), $parm[1]);
4978			parse_str(str_replace('%%__pipe__%%', '|', $parm[1]), $params);
4979			foreach ($params as $k => $v)
4980			{
4981				$params[str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $k)] = str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $v);
4982			}
4983		}
4984		else
4985		{
4986			$parmstr = '';
4987			$params = array();
4988		}
4989
4990		return array(1 => $multi, 2 => $params, 3 => $parmstr);
4991	}
4992
4993
4994	/**
4995	 * Remove Social Media Trackers from a $_GET array based on key matches.
4996	 * @param array $get
4997	 * @return array
4998	 */
4999	public static function removeTrackers($get = array())
5000	{
5001		$trackers = array('fbclid','utm_source','utm_medium','utm_content','utm_campaign','elan', 'msclkid', 'gclid');
5002
5003		foreach($trackers as $val)
5004		{
5005			if(isset($get[$val]))
5006			{
5007				unset($get[$val]);
5008			}
5009		}
5010
5011		return $get;
5012
5013	}
5014
5015
5016}
5017