e107inc/e107

View on GitHub
e107_handlers/application.php

Summary

Maintainability
F
4 days
Test Coverage
F
37%
<?php
/*
 * e107 website system
 *
 * Copyright (C) 2008-2012 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
*/


/**
 * Class e_url
 * New v2.1.6
 */
class e_url
{

    private static $_instance;

    private $_request       = null;

    private $_config        = array();

    private $_include       = null;

    private $_rootnamespace = null;

    private $_alias         = array();

    private $_legacy        = array();

    private $_legacyAliases = array();


    /**
     * e_url constructor.
     */
    function __construct()
    {
        $this->_request         = (e_HTTP === '/') ? ltrim(e_REQUEST_URI,'/') : str_replace(e_HTTP,'', e_REQUEST_URI) ;

        $this->_config          = e107::getUrlConfig();
        $this->_alias           = e107::getPref('e_url_alias');

        $this->_rootnamespace   = e107::getPref('url_main_module');
        $this->_legacy          = e107::getPref('url_config');
        $this->_legacyAliases   = e107::getPref('url_aliases');


        $this->setRootNamespace();

    }

    /**
     * Detect older e_url system.
     * @return bool
     */
    private function isLegacy()
    {

        $arr = (!empty($this->_legacyAliases[e_LAN])) ?  array_merge($this->_legacy,$this->_legacyAliases[e_LAN]) : $this->_legacy;

        $list = array_keys($arr);

        foreach($list as $leg)
        {
            if(strpos($this->_request,$leg.'/') === 0 || $this->_request === $leg)
            {
                return true;
            }

        }

        return false;
    }


    /**
     * @return string|null
     */
    public function getInclude()
    {
        return $this->_include;
    }


    /**
     * @return void
     */
    private function setRootNamespace()
    {

        $plugs = array_keys($this->_config);

        if(!empty($this->_rootnamespace) && in_array($this->_rootnamespace,$plugs)) // Move rootnamespace check to the end of the list.
        {
            $v = $this->_config[$this->_rootnamespace];
            unset($this->_config[$this->_rootnamespace]);
            $this->_config[$this->_rootnamespace] = $v;
        }

    }


    /**
     * @return bool|void
     */
    public function run()
    {
        $pref = e107::getPref();
        $tp = e107::getParser();

        if(empty($this->_request)) // likely 'index.php' ie. the home page.
        {
            $this->_request = e107::getFrontPage();
            e107::canonical('_SITEURL_');
        }

        if(empty($this->_config) /*|| empty($this->_request) || $this->_request === 'index.php'*/ || $this->isLegacy() === true)
        {
            return false;
        }

        $replaceAlias = array('{alias}\/?','{alias}/?','{alias}\/','{alias}/',);

        foreach($this->_config as $plug=>$cfg)
        {
            if(empty($pref['e_url_list'][$plug])) // disabled.
            {
                e107::getDebug()->log('e_URL for <b>'.$plug.'</b> is disabled.');
                continue;
            }

            foreach($cfg as $k=>$v)
            {

                if(empty($v['regex']))
                {
                //    e107::getMessage()->addDebug("Skipping empty regex: <b>".$k."</b>");
                    continue;
                }

                if((empty($v['domain']) && (empty($this->_request) || $this->_request === 'index.php')) || (isset($v['domain']) && ($v['domain'] !== $_SERVER['HTTP_HOST'])))
                {
                    continue;
                }

                if(!empty($v['alias']))
                {
                    $alias = (!empty($this->_alias[e_LAN][$plug][$k])) ? $this->_alias[e_LAN][$plug][$k] : $v['alias'];
                //    e107::getMessage()->addDebug("e_url alias found: <b>".$alias."</b>");
                    if(!empty($this->_rootnamespace) && $this->_rootnamespace === $plug)
                    {
                        $v['regex'] = str_replace($replaceAlias, '', $v['regex']);
                    }
                    else
                    {

                        $v['regex'] = str_replace('{alias}', $alias, $v['regex']);
                    }
                }


                $regex = '#'.$v['regex'].'#';

                if(empty($v['redirect']))
                {
                    continue;
                }


                $newLocation = preg_replace($regex, $v['redirect'], $this->_request);

                if($newLocation != $this->_request)
                {
                    $redirect = e107::getParser()->replaceConstants($newLocation);
                    list($file,$query) = explode("?", $redirect,2);

                    $get = array();
                    if(!empty($query))
                    {
                        // issue #3171 fix double ampersand in case of wrong query definition
                        $query = str_replace('&&', '&', $query);
                        parse_str($query,$get);
                    }


                    foreach($get as $gk=>$gv)
                    {
                        $_GET[$gk] = $gv;
                    }

                    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);

                    if(file_exists($file))
                    {
                        define('e_CURRENT_PLUGIN', $plug);
                        define('e_QUERY', str_replace('&&', '&', $query)); // do not add to e107_class.php
                        define('e_URL_LEGACY', $redirect);
                        if(!defined('e_PAGE'))
                        {
                            define('e_PAGE', basename($file));
                        }

                        if(!e107::route()) // subject to removal at any time.
                        {
                            e107::route($plug.'/'.$k);
                        }

                        $fpUrl = str_replace(SITEURL, '', rtrim(e_REQUEST_URL, '?/'));
                        $fpPref = e107::getFrontpage();

                        if($fpUrl === $fpPref)
                        {

                        }
                        unset($fpUrl, $fpPref);


                        $this->_include= $file;
                        return true;
                    //    exit;
                    }
                    elseif(getperms('0'))
                    {

                        echo "<div class='alert alert-warning'>";
                        echo "<h3>SEF Debug Info</h3>";
                        echo "File missing: ".$file;
                        echo "<br />Matched key: <b>".$k."</b>";
                        print_a($v);
                        echo "</div>";

                    }

                }
            }

        }




    }


    /**
     * Singleton implementation
     * @return e_url
     */
    public static function instance()
    {
        if(null == self::$_instance)
        {
            self::$_instance = new self();
        }
          return self::$_instance;
    }



}




 
/**
 * e107 Front controller
 */
class eFront
{
    /**
     * Singleton instance
     * @var eFront
     */
    private static $_instance;
    
    /**
     * @var eDispatcher
     */
    protected $_dispatcher;
    
    /**
     * @var eRequest
     */
    protected $_request; 
    
    /**
     * @var eRouter
     */
    protected $_router;


    protected $_response;
    
    /**
     * @var string path to file to include - the old deprecated way of delivering content
     */
    protected static $_legacy = '';
    
    /**
     * Constructor
     */
    private function __construct()
    {
    }
    
    /**
     * Cloning not allowed
     *
     */
    private function __clone()
    {
    }
    
    /**
     * Singleton implementation
     * @return eFront
     */
    public static function instance()
    {
        if(null == self::$_instance)
        {
            self::$_instance = new self();
        }
          return self::$_instance;
    }
    
    /**
     * Dispatch
     */
    public function dispatch(eRequest $request = null, eResponse $response = null, eDispatcher $dispatcher = null)
    {
        if(null === $request)
        {
            if(null === $this->getRequest()) 
            {
                $request = new eRequest();
                $this->setRequest($request);
            }
            else $request = $this->getRequest();
        }
        elseif(null === $this->getRequest()) $this->setRequest($request);
        
        if(null === $response)
        {
            if(null === $this->getResponse()) 
            {
                $response = new eResponse();
                $this->setResponse($response);
            }
            else $response = $this->getResponse();
        }
        elseif(null === $this->getRequest()) $this->setRequest($request);
        
        
        if(null === $dispatcher)
        {
            if(null === $this->getDispatcher()) 
            {
                $dispatcher = new eDispatcher();
                $this->setDispatcher($dispatcher);
            }
            else $dispatcher = $this->getDispatcher();
        }
        elseif(null === $this->getDispatcher()) $this->setDispatcher($dispatcher);
        
        
        // set dispatched status true, required for checkLegacy()
        $request->setDispatched(true);

        $router = $this->getRouter();

        // If current request not already routed outside the dispatch method, route it
        if(!$request->routed) $router->route($request); 

        $c = 0;
        // dispatch loop
        do 
        {
            $c++;
            if($c > 100)
            {
                throw new eException("Too much dispatch loops", 1);
            }
            
            // dispatched status true on first loop
            $router->checkLegacy($request);
            
            // dispatched by default - don't allow legacy to alter dispatch status
            $request->setDispatched(true);
            
            // legacy mod - return control to the bootstrap
            if(self::isLegacy()) 
            {
                return; 
            }
            
            // for the good players - dispatch loop - no more BC!
            try 
            {
                $dispatcher->dispatch($request, $response);
            }
            catch(eException $e)
            {
                echo $request->getRoute().' - '.$e->getMessage();
                exit;
            }

            
        } while (!$request->isDispatched());
    }
    
    /**
     * Init all objects required for request dispatching
     * @return eFront
     */
    public function init()
    {
        $request = new eRequest();
        $this->setRequest($request);
        
        $dispatcher = new eDispatcher();
        $this->setDispatcher($dispatcher);
        
        $router = new eRouter();
        $this->setRouter($router);

        /** @var eResponse $response */
        $response = e107::getSingleton('eResponse');
        $this->setResponse($response);
        
        return $this;
    }
    
    /**
     * Dispatch
     * @param string|eRequest $route
     */
    public function run($route = null)
    {
        if($route) 
        {
            if(is_object($route) && ($route instanceof eRequest)) $this->setRequest($route);
            elseif(null !== $route && null !== $this->getRequest()) $this->getRequest()->setRoute($route);
        }
        try 
        {
            $this->dispatch();
        }
        catch(eException $e)
        {
            echo $e->getMessage();
            exit;
        }
        
    }
    
    /**
     * Application instance (e107 class)
     * @return e107
     */
    public static function app()
    {
        return e107::getInstance();
    }
    
    /**
     * Get dispatcher instance
     * @return eDispatcher
     */
    public function getDispatcher()
    {
        return $this->_dispatcher;
    }
    
    /**
     * Set dispatcher
     * @param eDispatcher $dispatcher
     * @return eFront
     */
    public function setDispatcher(eDispatcher $dispatcher)
    {
        $this->_dispatcher = $dispatcher;
        return $this;
    }
    
    /**
     * Get request instance
     * @return eRequest
     */
    public function getRequest()
    {
        return $this->_request;
    }
    
    /**
     * Set request
     * @param eRequest $request
     * @return eFront
     */
    public function setRequest(eRequest $request)
    {
        $this->_request = $request;
        return $this;
    }
    
    /**
     * Get response instance
     * @return eResponse
     */
    public function getResponse()
    {
        return $this->_response;
    }
    
    /**
     * Set response
     * @param eResponse $response
     * @return eFront
     */
    public function setResponse(eResponse $response)
    {
        $this->_response = $response;
        return $this;
    }
    
    /**
     * Get router instance
     * @return eRouter
     */
    public function getRouter()
    {
        return $this->_router;
    }
    
    /**
     * Set router instance
     * @return eFront
     */
    public function setRouter(eRouter $router)
    {
        $this->_router = $router;
        return $this;
    }
    
    /**
     * Set/get legacy status of the current request
     * @param boolean $status
     * @return boolean
     */
    public static function isLegacy($status = null)
    {
        if(null !== $status) 
        {
            if(!empty($status[0]) && ($status[0] === '{'))
            {
                $status = e107::getParser()->replaceConstants($status);
            }
            self::$_legacy = $status;
        }
        return self::$_legacy;
    }
}

/**
 * e107 Dispatcher
 * It decides how to dispatch the request.
 */
class eDispatcher
{
    protected static $_configObjects = array();

    /**
     * @param eRequest|null $request
     * @param eResponse|null $response
     * @return void
     * @throws eException
     */
    public function dispatch(eRequest $request = null, eResponse $response = null)
    {
        $controllerName = $request->getControllerName();
        $moduleName = $request->getModuleName();
        $className = $this->isDispatchable($request, false);
        
        // dispatch based on rule settings
        if(!$className)
        {
            if($controllerName == 'index') // v2.x upgrade has not been run yet. 
            {
                e107::getRedirect()->redirect(e_ADMIN."admin.php");    
            }
            
            throw new eException("Invalid controller '".$controllerName."'");
        }
        
        $controller = new $className($request, $response);
        if(!($controller instanceof eController))
        {
            throw new eException("Controller $controller is not an instance of eController");
        }
        
        $actionName = $request->getActionMethodName();
        
        ob_start();
        
        $controller->dispatch($actionName);
        
        $content = ob_get_clean();

        $response->appendBody($content);
        unset($controller);
    }
    
    /**
     * Get path to the e_url handler
     * @param string $module 
     * @param string $location plugin|core|override
     * @param boolean $sc
     * @return string path
     */
    public static function getConfigPath($module, $location, $sc = false)
    {
        $tmp = explode('/', $location);
        $custom = '';
        $location = $tmp[0];
        if(isset($tmp[1]) && !empty($tmp[1])) 
        {
            $custom = $tmp[1].'_';
        }
        unset($tmp);
        if($module !== '*') $module .= '/';
        
        switch ($location) 
        {
            case 'plugin':
                //if($custom) $custom = 'url/'.$custom;
                if(!defined('e_CURRENT_PLUGIN'))
                {
                    define('e_CURRENT_PLUGIN', rtrim($module,'/')); // TODO Move to a better location.
                }
                return $sc ? '{e_PLUGIN}'.$module.'url/'.$custom.'url.php' : e_PLUGIN.$module.'url/'.$custom.'url.php';
            break;

            case 'core':
                if($module === '*') return $sc ? '{e_CORE}url/' : e_CORE.'url/';
                return $sc ? '{e_CORE}url/'.$module.$custom.'url.php' : e_CORE.'url/'.$module.$custom.'url.php';
            break;

            case 'override':
                if($module === '*') return $sc ? '{e_CORE}override/url/'  : e_CORE.'override/url/' ;
                return $sc ? '{e_CORE}override/url/'.$module.$custom.'url.php'  : e_CORE.'override/url/'.$module.$custom.'url.php' ;
            break;
            
            default:
                return null;
            break;
        }
    }
    
    /**
     * Get path to url configuration subfolders
     * @param string $module 
     * @param string $location plugin|core|override
     * @param boolean $sc
     * @return string path
     */
    public static function getConfigLocationPath($module, $location, $sc = false)
    {
        switch ($location) 
        {
            case 'plugin':
                return $sc ? '{e_PLUGIN}'.$module.'/url/' : e_PLUGIN.$module.'/url/';
            break;

            case 'core':
                return $sc ? '{e_CORE}url/'.$module.'/' : e_CORE.'url/'.$module.'/';
            break;

            case 'override':
                return $sc ? '{e_CORE}override/url/'.$module.'/'  : e_CORE.'override/url/'.$module.'/';
            break;
            
            default:
                return null;
            break;
        }
    }
    
    /**
     * Get dispatch system path
     * @param string $location plugin|core|override
     * @param string $plugin required only when $location is plugin
     * @param boolean $sc
     * @return string path
     */
    public static function getDispatchLocationPath($location, $plugin = null, $sc = false)
    {    
        switch ($location) 
        {
            case 'plugin':
                if(!$plugin) return null;
                return $sc ? '{e_PLUGIN}'.$plugin.'/controllers/' : e_PLUGIN.$plugin.'/controllers/';
            break;
            
            case 'core':
                return $sc ? '{e_CORE}controllers/' : e_CORE.'controllers/';
            break;
            
            case 'override':
                return $sc ? '{e_CORE}override/controllers/' : e_CORE.'override/controllers/';
            break;
            
            default:
                return null;
            break;
        }
    }
    
    /**
     * Get full dispatch system path
     * @param string $module 
     * @param string $location plugin|core|override
     * @param boolean $sc
     * @return string path
     */
    public static function getDispatchPath($module, $location, $sc = false)
    {    
        switch ($location) 
        {
            case 'plugin':
                return $sc ? '{e_PLUGIN}'.$module.'/controllers/' : e_PLUGIN.$module.'/controllers/';
            break;
            
            case 'core':
                return $sc ? '{e_CORE}controllers/'.$module.'/' : e_CORE.'controllers/'.$module.'/';
            break;
            
            case 'override':
                return $sc ? '{e_CORE}override/controllers/'.$module.'/' : e_CORE.'override/controllers/'.$module.'/';
            break;
            
            default:
                return null;
            break;
        }
    }
    
    /**
     * Get include path to a given module/controller
     * 
     * @param string $module valid module name
     * @param string $controller controller name
     * @param string $location core|plugin|override
     * @param boolean $sc return relative (false) OR shortcode (true) path
     * @return string controller path
     */
    public static function getControllerPath($module, $controller, $location = null, $sc = false)
    {
        if(null === $location) $location = self::getDispatchLocation($module);
        
        return ($location ? self::getDispatchPath($module, $location, $sc).$controller.'.php': null);
    }
    
    /**
     * Get class name of a given module/controller
     * 
     * @param string $module valid module name
     * @param string $controllerName controller name
     * @param string $location core|plugin|override
     * @return string controller path
     */
    public static function getControllerClass($module, $controllerName, $location = null)
    {
        if(null === $location) $location = self::getDispatchLocation($module);
        
        return ($location ? $location.'_'.$module.'_'.$controllerName.'_controller' : null);
    }
    

    /**
     * Get controller object
     * 
     * @param eRequest $request
     * @param boolean $checkOverride whether to check the override location
     * @return eController null if not dispatchable
     */
    public function getController(eRequest $request, $checkOverride = true)
    {
        $class_name = $this->isDispatchable($request, true, $checkOverride);
        if(!$class_name) return null;
        
        return new $class_name();
    }
    
    /**
     * Check if given module/controller is dispatchable
     * @param string $module valid module name
     * @param string $controllerName controller name
     * @param string $location core|plugin|override
     * @param boolean $checkOverride whether to check the override location
     * @return string class name OR false if not dispatchable
     */
    public function isDispatchableModule($module, $controllerName, $location, $checkOverride = false)
    {
        if($checkOverride || $location == 'override')
        {
            $path = self::getControllerPath($module, $controllerName, 'override', false); 
            
            $class_name = self::getControllerClass($module, $controllerName, 'override');
            if($class_name && !class_exists($class_name, false) && is_readable($path)) include_once($path);
            
            if($class_name && class_exists($class_name, false)) return $class_name;
        }
        
        // fallback to original dispatch location if any
        if($location === 'override')
        {
            // check for real location
            if(($location = eDispatcher::getModuleRealLocation($module)) === null) return false;
        }
        
        if($location !== 'override')
        {
            $path = self::getControllerPath($module, $controllerName, $location, false); 
            
            $class_name = self::getControllerClass($module, $controllerName, $location);
            if(!$class_name) return false;
            
            if(!class_exists($class_name, false) && is_readable($path)) include_once($path);
            
            if(class_exists($class_name, false)) return $class_name;
        }
        return false;
    }
    
    /**
     * Automated version of self::isDispatchableModule()
     * @param eRequest $request 
     * @param boolean $checkReflection deep check - proper subclassing, action
     * @param boolean $checkOverride try override controller folder first
     * @return false|string class name OR false if not dispatchable
     */
    public function isDispatchable(eRequest $request, $checkReflection = false, $checkOverride = true)
    {
        $location = self::getDispatchLocation($request->getModuleName());
        
        $controllerName = $request->getControllerName();
        $moduleName = $request->getModuleName();
        $className = false;
        
        // dispatch based on url_config preference value, if config location is override and there is no
        // override controller, additional check against real controller location will be made
        if($location)
        {
            $className = $this->isDispatchableModule($moduleName, $controllerName, $location, $checkOverride);
        }
        //else 
        //{
            # Disable plugin check for routes with no config info - prevent calling of non-installed plugins
            # We may allow this for plugins which don't have plugin.xml in the future 
            // $className = $this->isDispatchableModule($moduleName, $controllerName, 'plugin', $checkOverride);
            // if(!$className)  
            //$className = $this->isDispatchableModule($moduleName, $controllerName, 'core', $checkOverride);
        //}
        
        if(empty($className)) return false;
        elseif(!$checkReflection) return $className;
        
        $rfl = new ReflectionClass($className);
        $method = $request->getActionMethodName();
        if($rfl->isSubclassOf('eController') && $rfl->hasMethod($method) && $rfl->getMethod($method)->isPublic() && !$rfl->getMethod($method)->isStatic())
            return $className;
        
        return false;
    }
    
    /**
     * Get class name of a given module config
     * 
     * @param string $module valid module name
     * @param string $location core|plugin|override[/custom]
     * @return string controller path
     */
    public static function getConfigClassName($module, $location)
    {
        $tmp = explode('/', $location);
        $custom = '';
        $location = $tmp[0];
        if(isset($tmp[1]) && !empty($tmp[1])) 
        {
            $custom = $tmp[1].'_';
        }
        unset($tmp);
        $module .= '_';
        
        // we need to prepend location to avoid namespace colisions
        return $location.'_'.$module.$custom.'url';
    }
    
    /**
     * Get config object for a module
     * 
     * @param string $module valid module name
     * @param string $location core|plugin|override[/custom]
     * @return eUrlConfig
     */
    public static function getConfigObject($module, $location = null)
    {
        if(null === $location)
        {
            $location = self::getModuleConfigLocation($module);
            if(!$location) return null;
        }
        $reg = $module.'/'.$location;
        if(isset(self::$_configObjects[$reg])) return self::$_configObjects[$reg];
        $className = self::getConfigClassName($module, $location); 
        if(!class_exists($className, false))
        {
            $path = self::getConfigPath($module, $location, false);
            if(!is_readable($path)) return null;
            include_once($path);
            
            if(!class_exists($className, false)) return null;
        }

        /** @var eUrlConfig $obj */
        $obj = new $className();
        $obj->init();
        self::$_configObjects[$reg] = $obj;
        $obj = null;
        
        return self::$_configObjects[$reg];
    }

    /**
     * Auto discover module location from stored in core prefs data
     * @param string $module
     * @return mixed
     */
    public static function getModuleConfigLocation($module)
    {
        //retrieve from config prefs
        return e107::findPref('url_config/'.$module, '');
    }

    /**
     * Auto discover module location from stored in core prefs data
     * @param string $module
     * @return mixed|null|string
     */
    public static function getDispatchLocation($module)
    {
        //retrieve from prefs
        $location = self::getModuleConfigLocation($module);
        if(!$location) return null;
        
        if(($pos = strpos($location, '/'))) //can't be 0
        {
            return substr($location, 0, $pos);
        }
        return $location;
    }
    
    
    /**
     * Auto discover module real location (and not currently set from url adminsitration) from stored in core prefs data
     * @param string $module
     */
    public static function getModuleRealLocation($module)
    {
        //retrieve from prefs
        $searchArray = e107::findPref('url_modules');
        if(!$searchArray) return null;
        
        $search = array('core', 'plugin', 'override');
        
        foreach ($search as $location) 
        {
            $_searchArray = vartrue($searchArray[$location], array());
            if(in_array($module, $_searchArray)) return $location;
        }
        return null;
    }
}

/**
 * URL manager - parse and create URLs based on rules set
 * Inspired by Yii Framework UrlManager <www.yiiframework.com>
 */
class eRouter
{
    /**
     * Configuration array containing all available syste routes and route object configuration values
     * @var array
     */
    protected $_rules = array();
    
    /**
     * List of all system wide available aliases
     * This includes multi-lingual configurations as well
     * @var array
     */
    protected $_aliases = array();
    
    /**
     * Cache for rule objects
     * @var array
     */
    protected $_parsedRules = array(); // array of rule objects
    
    /**
     * Global config values per rule set
     * @var array
     */
    protected $_globalConfig = array(); 
    
    /**
     * Module name which is used for site main namespace
     * Example mysite.com/news/News Item => converted to mysite.com/News Item
     * NOTE: Could be moved to rules config
     * 
     * @var string 
     */
    protected $_mainNsModule = '';
    
    /**
     * Default URL suffix - to be added to end of all urls (e.g. '.html')
     * This value can be overridden per rule item
     * NOTE could be moved to rules config only
     * @var string
     */
    public $urlSuffix = '';
    
    /**
     * @var string  GET variable name for route. Defaults to 'route'.
     */
    public $routeVar = 'route';
    
    /**
     * @var string
     */
    const FORMAT_GET = 'get';
    
    /**
     * @var string
     */
    const FORMAT_PATH = 'path';
    
    /**
     * @var string
     */
    private $_urlFormat = self::FORMAT_PATH;
    
    /**
     * Not found route
     * @var string
     */
    public $notFoundRoute = 'system/error/notfound';
    
    protected $_defaultAssembleOptions = array('full' => false, 'amp' => '&amp;', 'equal' => '=', 'encode' => true);
    
    /**
     * Not found URL - used when system route not found and 'url_error_redirect' core pref is true
     * TODO - user friendly URL ('/system/404') when system config is ready ('/system/404')
     * @var string
     */
    public $notFoundUrl = 'system/error/404?type=routeError';



    
    public function __construct()
    {
        $this->_init();
    }
    
    /**
     * Init object
     * @return void 
     */
    protected function _init()
    {
        // Gather all rules, add-on info, cache, module for main namespace etc
        $this->loadConfig()
            ->setAliases();
        // we need config first as setter does some checks if module can be set as main
        $this->setMainModule(e107::getPref('url_main_module', ''));
    }
    
    /**
     * Set module for default namespace
     * @param string $module
     * @return eRouter
     */
    public function setMainModule($module)
    {
        if(!$module || !$this->isModule($module) || !$this->getConfigValue($module, 'allowMain')) return $this;
        $this->_mainNsModule = $module;
        return $this;
    }
    
    /**
     * Get main url namespace module
     * @return string
     */
    public function getMainModule()
    {
        return $this->_mainNsModule;
    }
    
    /**
     * Check if given module is the main module
     * @param string $module
     * @return boolean
     */
    public function isMainModule($module)
    {
        return ($this->_mainNsModule === $module);
    }
    
    
    /**
     * @return string get|path
     */
    public function getUrlFormat()
    {
        return $this->_urlFormat;
    }


    /**
     * Load config and url rules, if not available - build it on the fly
     * @return eRouter
     */
    public function loadConfig($forceRebuild = false)
    {

        if(!is_readable(e_CACHE_URL . 'config.php') || $forceRebuild == true)
        {
            $config = $this->buildGlobalConfig();
        }
        else
        {
            $config = include(e_CACHE_URL . 'config.php');
        }

        if(empty($config))
        {
            trigger_error('URL Config is empty', E_USER_NOTICE);
            $config = array();
        }
        
        $rules = array();
        
        foreach ($config as $module => $c) 
        {
            $rules[$module] = $c['rules'];
            unset($config[$module]['rules']);
            $config[$module] = $config[$module]['config'];
        }
        $this->_globalConfig = $config;

        $this->setRuleSets($rules);

        return $this;
    }

    /**
     * @return void
     */
    public static function clearCache()
    {
        if(file_exists(e_CACHE_URL.'config.php'))
        {
            @unlink(e_CACHE_URL.'config.php');    
        }
    }

    /**
     * Build unified config.php
     */
    public function buildGlobalConfig($save = true)
    {
        $active = e107::getPref('url_config', array());

        if(empty($active))
        {
            trigger_error('url_config pref is empty', E_USER_NOTICE);
        }
        
        $config = array();
        foreach ($active as $module => $location) 
        {
            $_config = array();
            $obj = eDispatcher::getConfigObject($module, $location);
            $path = eDispatcher::getConfigPath($module, $location, true);
            
            if(null !== $obj)
            {
                $_config = $obj->config();
                $_config['config']['configPath'] = $path;
                $_config['config']['configClass'] = eDispatcher::getConfigClassName($module, $location);
            }
            if(!isset($_config['config'])) $_config['config'] = array();
            
            $_config['config']['location'] = $location;
            if(!isset($_config['config']['format']) || !in_array($_config['config']['format'], array(self::FORMAT_GET, self::FORMAT_PATH)))
            {
                $_config['config']['format'] = $this->getUrlFormat();
            }

            if(!isset($_config['rules'])) $_config['rules'] = array();
            
            foreach ($_config['rules'] as $pattern => $rule) 
            {
                if(!is_array($rule))
                {
                    $_config['rules'][$pattern] = array($rule);
                }
            }
            
            $config[$module] = $_config;
        }
        
        if($save)
        {
            $fileContent = '<?php'."\n### Auto-generated - DO NOT EDIT ### \nreturn ";
            $fileContent .= trim(var_export($config, true)).';';
            
            file_put_contents(e_CACHE_URL.'config.php', $fileContent);
        }
        return $config;
    }

    /**
     * Retrieve config array from a given system path
     * @param string $path
     * @param string $location core|plugin|override
     */
    public static function adminReadConfigs($path, $location = null)
    {
        $file = e107::getFile(false);
        $ret = array();
        
        $file->mode = 'fname';
        $files = $file->setFileInfo('fname')
            ->get_files($path, '^([a-z_]{1,}_)?url\.php$');
            
        
        foreach ($files as $file) 
        {
            if(null === $location)
            {
                $c = eRouter::file2config($file, $location);
                if($c) $ret[] = $c;
                continue;
            }
            $ret[] = eRouter::file2config($file, $location);
        }
        return $ret;
    }

    /**
     * Convert filename to configuration string
     * @param string $filename
     * @param string $location core|plugin|override
     */
    public static function file2config($filename, $location = '')
    {
        if($filename == 'url.php') return $location;
        if($location) $location .= '/';
        return $location.substr($filename, 0, strrpos($filename, '_'));
    }
    
    /**
     * Detect all available system url modules, used as a map on administration configuration path
     * and required (same structure) {@link from eDispatcher::adminBuildConfig())
     * This is a very liberal detection, as it doesn't require config file.
     * It goes through both config and dispatch locations and registers directory tree as modules
     * The only exception are plugins - if plugin requires install (plugin.xml) and it is not installed,
     * it won't be registered
     * Another important thing is - core has always higher priority, as plugins are not allowed to 
     * directly override core modules. At this moment, core modules could be overloaded only via override configs (e107_core/override/url/) 
     * and controllers (e107_core/override/controllers) 
     * This array is stored as url_modules core preference 
     * 
     * @param string $type possible values are all|plugin|core|override
     * @return array available system url modules stored as url_modules core preference
     */    
    public static function adminReadModules($type = 'all')
    {
        $f = e107::getFile();
        $ret = array('core' => array(), 'plugin' => array(), 'override' => array());
        $plugins = array();
        
        if($type == 'all' || $type = 'core')
        {
            $location = eDispatcher::getDispatchLocationPath('core');
            // search for controllers first
            $ret['core'] = $f->get_dirs($location);
            
            // merge with configs
            $configArray = $f->get_dirs(eDispatcher::getConfigPath('*', 'core'));
            foreach ($configArray as $config) 
            {
                if(!in_array($config, $ret['core']))
                {
                    $ret['core'][] = $config;
                }
            }
            sort($ret['core']);
        }
        
        if($type == 'all' || $type = 'plugin')
        {
            $plugins = $f->get_dirs(e_PLUGIN);
            foreach ($plugins as $plugin) 
            {
                // DON'T ALLOW PLUGINS TO OVERRIDE CORE!!! 
                // This will be possible in the future under some other, more controllable form
                if(in_array($plugin, $ret['core'])) continue;
                
                $location = eDispatcher::getDispatchLocationPath('plugin', $plugin);
                $config = eDispatcher::getConfigPath($plugin, 'plugin');
                
                if(e107::isInstalled($plugin))
                {
                    if(is_dir($location) || is_readable($config))
                    {
                        $ret['plugin'][] = $plugin;
                    }
                    continue;
                }
                
                // Register only those who don't need install and may be dispatchable
                if((!is_readable(e_PLUGIN.$plugin.'/plugin.php') && !is_readable(e_PLUGIN.$plugin.'/plugin.xml')))
                {
                    if(is_dir($location) || is_readable($config))
                    {
                        $ret['plugin'][] = $plugin;
                    }
                }
            }
            sort($ret['plugin']);
        }
        
        if($type == 'all' || $type = 'override')
        {
            // search for controllers first
            $location = eDispatcher::getDispatchLocationPath('override');
            $ret['override'] = $f->get_dirs($location);
            
            // merge with configs
            $configArray = $f->get_dirs(eDispatcher::getConfigPath('*', 'override'));
            foreach ($configArray as $config) 
            {
                if(!in_array($config, $ret['override']))
                {
                    $ret['override'][] = $config;
                }
            }
            sort($ret['override']);
            
            // remove not installed plugin locations, possible only for 'all' type
            if($type == 'all')
            {
                foreach ($ret['override'] as $i => $l) 
                {
                    // it's a plugin override, but not listed in current plugin array - remove
                    if(in_array($l, $plugins) && !in_array($l, $ret['plugin']))
                    {
                        unset($ret['override'][$i]);
                    }
                }
            }
        }
        
        return $ret;
    }

    /**
     * Rebuild configuration array, stored as url_config core preference
     * More strict detection compared to {@link eDispatcher::adminReadModules()}
     * Current flat array containing config locations per module are rebuilt so that new 
     * modules are registered, missing modules - removed. Additionally fallback to the default location 
     * is done if current user defined location is not readable
     * @see eDispatcher::adminReadModules()
     * @param array current configuration array (url_config core preference like)
     * @param array available URL modules as detected by {@link eDispatcher::adminReadModules()} and stored as url_modules core preference value
     * @return array new url_config array
     */
    public static function adminBuildConfig($current, $adminReadModules = null)
    {
        if(null === $adminReadModules) $adminReadModules = self::adminReadModules();
        
        $ret = array();
        $all = array_unique(array_merge($adminReadModules['core'], $adminReadModules['plugin'], $adminReadModules['override']));
        foreach ($all as $module) 
        {
            if(isset($current[$module]))
            {
                // current contains custom (readable) config location e.g. news => core/rewrite
                if(strpos($current[$module], '/') !== false && is_readable(eDispatcher::getConfigPath($module, $current[$module])))
                {
                    $ret[$module] = $current[$module];
                    continue;
                }
                
                // in all other cases additional re-check will be made - see below
            }
            
            if(in_array($module, $adminReadModules['override']))
            {
                // core check
                if(in_array($module, $adminReadModules['core']))
                {
                    $mustHave = is_readable(eDispatcher::getConfigPath($module, 'core'));
                    $has = is_readable(eDispatcher::getConfigPath($module, 'override'));
                    
                    // No matter if it must have, it has e_url config
                    if($has) $ret[$module] = 'override';
                    // It must have but it doesn't have e_url config, fallback
                    elseif($mustHave && !$has) $ret[$module] = 'core';
                    // Rest is always core as controller override is done on run time
                    else $ret[$module] = 'core';
                }
                // plugin check
                elseif(in_array($module, $adminReadModules['plugin']))
                {
                    $mustHave = is_readable(eDispatcher::getConfigPath($module, 'plugin'));
                    $has = is_readable(eDispatcher::getConfigPath($module, 'override'));
                    
                    // No matter if it must have, it has e_url config
                    if($has) $ret[$module] = 'override';
                    // It must have but it doesn't have e_url config, fallback
                    elseif($mustHave && !$has) $ret[$module] = 'plugin';
                    // Rest is always plugin as config is most important, controller override check is done on run time
                    else $ret[$module] = 'plugin';
                }
                // standalone override module
                else
                {
                    $ret[$module] = 'override';
                }
                
            }
            // default core location 
            elseif(in_array($module, $adminReadModules['core']))
            {
                $ret[$module] = 'core';
            }
            // default plugin location
            elseif(in_array($module, $adminReadModules['plugin']))
            {
                $ret[$module] = 'plugin';
            }
        }
        return $ret;
    }

    /**
     * Detect available config locations (readable check), based on available url_modules {@link eDispatcher::adminReadModules()} core preference arrays 
     * Used to rebuild url_locations core preference value
     * @see eDispatcher::adminBuildConfig()
     * @see eDispatcher::adminReadModules()
     * @param array $available {@link eDispatcher::adminReadModules()} stored as url_modules core preference
     * @return array available config locations, stored as url_locations core preference
     */
    public static function adminBuildLocations($available = null)
    {
        $ret = array();
        if(null === $available) $available = self::adminReadModules();
        
        $fl = e107::getFile();
        
        // Core
        foreach ($available['core'] as $module) 
        {
            // Default module
            $ret[$module] = array('core');
            
            // read sub-locations
            $path = eDispatcher::getConfigLocationPath($module, 'core'); 
            //$sub = $fl->get_dirs($path);
            $sub = eRouter::adminReadConfigs($path);
            
            if($sub)
            {
                foreach ($sub as $moduleSub)
                {
                    // auto-override: override available (controller or url config), check for config
                    if(in_array($module, $available['override']) && is_readable(eDispatcher::getConfigPath($module, 'override/'.$moduleSub)))
                    {
                        $ret[$module][] = 'override/'.$moduleSub;
                    }
                    // no override available, register the core location
                    elseif(is_readable(eDispatcher::getConfigPath($module, 'core/'.$moduleSub))) 
                    {
                        $ret[$module][] = 'core/'.$moduleSub;
                    }
                } 
            }
        }
        
        
        // Plugins
        foreach ($available['plugin'] as $module) 
        {
            // Default module
            $ret[$module] = array('plugin');
            
            // read sub-locations
            $path = eDispatcher::getConfigLocationPath($module, 'plugin');
            //$sub = $fl->get_dirs($path);
            $sub = eRouter::adminReadConfigs($path);
            
            if($sub)
            {
                foreach ($sub as $moduleSub)
                {
                    // auto-override: override available (controller or url config), check for config
                    if(in_array($module, $available['override']) && is_readable(eDispatcher::getConfigPath($module, 'override/'.$moduleSub)))
                    {
                        $ret[$module][] = 'override/'.$moduleSub;
                    }
                    // no override available, register the core location
                    elseif(is_readable(eDispatcher::getConfigPath($module, 'plugin/'.$moduleSub))) 
                    {
                        $ret[$module][] = 'plugin/'.$moduleSub;
                    }
                } 
            }
        }

        // Go through all overrides, register those who don't belong to core & plugins as standalone core modules
        foreach ($available['override'] as $module) 
        {
            // either it is a core/plugin module or e_url.php is not readable - continue
            if(in_array($module, $available['core']) || in_array($module, $available['plugin']))
            {
                continue;
            }
            
            // Default module
            $ret[$module] = array('override');
            
            // read sub-locations
            $path = eDispatcher::getConfigLocationPath($module, 'override');
            //$sub = $fl->get_dirs($path);
            $sub = eRouter::adminReadConfigs($path);
            
            if($sub)
            {
                foreach ($sub as $moduleSub)
                {
                    if(is_readable(eDispatcher::getConfigPath($module, 'override/'.$moduleSub))) 
                    {
                        $ret[$module][] = 'override/'.$moduleSub;
                    }
                } 
            }
        }
        
        return $ret;
    }

    /**
     * Match current aliases against currently available module and languages 
     * @param array $currentAliases url_aliases core preference
     * @param array $currentConfig url_config core preference
     * @return array cleaned aliases
     */
    public static function adminSyncAliases($currentAliases, $currentConfig)
    {
        if(empty($currentAliases)) return array();
        
        $modules = array_keys($currentConfig);
        
        // remove non existing languages
        $lng = e107::getLanguage();
        $lanList = $lng->installed();
        
        if(is_array($currentAliases))
        {
            foreach ($currentAliases as $lanCode => $aliases) 
            {
                $lanName = $lng->convert($lanCode);
                if(!$lanName || !in_array($lanName, $lanList))
                {
                    unset($currentAliases[$lanCode]);
                    continue;
                }
                
                // remove non-existing modules
                foreach ($aliases as $alias => $module) 
                {
                    if(!isset($currentConfig[$module])) unset($currentAliases[$lanCode][$alias]);
                }
            }
        }
        return $currentAliases;
    }

    /**
     * Retrieve global configuration array for a single or all modules
     * @param string $module system module
     * @return array configuration
     */
    public function getConfig($module = null)
    {
        if(null === $module) return $this->_globalConfig;
        
        return isset($this->_globalConfig[$module]) ? $this->_globalConfig[$module] : array();    
    }
    
    /**
     * Retrieve single value from a module global configuration array
     * @param string $module system module
     * @return array configuration
     */
    public function getConfigValue($module, $key, $default = null)
    {
        return isset($this->_globalConfig[$module]) && isset($this->_globalConfig[$module][$key]) ? $this->_globalConfig[$module][$key] : $default;    
    }
    
    /**
     * Get system name of a module by its alias
     * Returns null if $alias is not an existing alias
     * @param string $alias
     * @param string $lan optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
     * @return string module
     */
    public function getModuleFromAlias($alias, $lan = null)
    {
        if($lan) return e107::findPref('url_aliases/'.$lan.'/'.$alias, null);
        return (isset($this->_aliases[$alias]) ? $this->_aliases[$alias] : null);
    }
    
    /**
     * Get alias name for a module
     * Returns null if module doesn't have an alias
     * @param string $module
     * @param string $lan optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
     * @return string alias 
     */
    public function getAliasFromModule($module, $lan = null)
    {
        if($lan) 
        {
            $aliases = e107::findPref('url_aliases/'.$lan, array());
            return (in_array($module, $aliases) ? array_search($module, $aliases) : null);
        }
        return (in_array($module, $this->_aliases) ? array_search($module, $this->_aliases) : null);
    }
    
    /**
     * Check if alias exists
     * @param string $alias
     * @param string $lan optional language alias. Example $lan = 'bg' (search for Bulgarian aliases)
     * @return boolean
     */
    public function isAlias($alias, $lan = null)
    {
        if($lan) 
        {
            $aliases = e107::findPref('url_aliases/'.$lan, array());
            return isset($aliases[$alias]);
        }
        return isset($this->_aliases[$alias]);
    }
    
    /**
     * Check if there is an alias for provided module
     * @param string $module
     * @param string $lan optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
     * @return boolean
     */
    public function hasAlias($module, $lan = null)
    {
        if($lan) 
        {
            $aliases = e107::findPref('url_aliases/'.$lan, array());
            return in_array($module, $aliases);
        }
        return in_array($module, $this->_aliases);
    }

    /**
     * Get all available module aliases
     * @param string|null $lanCode optional language alias check. Example $lan = 'bg' (search for Bulgarian aliases)
     * @return array
     */
    public function getAliases($lanCode = null)
    {
        if($lanCode)
        {
            return e107::findPref('url_aliases/'.$lanCode, array());
        }
        return $this->_aliases;
    }
    
    /**
     * Set module aliases
     * @param array $aliases
     * @return eRouter
     */
    public function setAliases($aliases = null)
    {
        if(null === $aliases)
        {
            $lanCode = e107::getLanguage()->convert(e_LANGUAGE); 
            
            $aliases = e107::findPref('url_aliases/'.$lanCode, array());
        }
        $this->_aliases = $aliases;

        return $this;
    }
    
    /**
     * Check if provided module is present in the rules config
     * @param string module
     * @return boolean
     */
    public function isModule($module)
    {
        return isset($this->_globalConfig[$module]);
    }
    
    /**
     * Check if the passed value is valid module or module alias, returns system module
     * or null on failure
     * @param string $module
     * @param boolean $strict check for existence if true
     * @return string module
     */
    public function retrieveModule($module, $strict = true)
    {
        if($this->isAlias($module)) 
            $module = $this->getModuleFromAlias($module);
        
        if($strict && (!$module || !$this->isModule($module))) 
            return null;

        return $module;
    }
    
    /**
     * Set rule config for this instance
     * @param array $rules
     * @return void
     */
    public function setRuleSets($rules)
    {
        $this->_rules = $rules;
    }
    
    /**
     * Retrieve rule set for a module
     * @param string $module
     */
    public function getRuleSet($module)
    {
        return (isset($this->_rules[$module]) ? $this->_rules[$module] : array());
    }
    
    /**
     * Get all rule sets
     */
    public function getRuleSets()
    {
        return $this->_rules;
    }
    
    /**
     * Retrive array of eUrlRule objects for given module
     */
    public function getRules($module)
    {
        return $this->_processRules($module);
    }
    
    /**
     * Process rule set array, create rule objects
     * TODO - rule cache
     * @param string $module
     * @return array processed rule set
     */
    protected function _processRules($module)
    {
        if(!$this->isModule($module)) return array();
        
        if(!isset($this->_parsedRules[$module]))
        {
            $rules = $this->getRuleSet($module);
            $config = $this->getConfig($module);
            $this->_parsedRules[$module] = array();
            $map = array('urlSuffix' => 'urlSuffix', 'legacy' => 'legacy', 'legacyQuery' => 'legacyQuery', 'mapVars' => 'mapVars', 'allowVars' => 'allowVars', 'matchValue' => 'matchValue');
            foreach ($rules as $pattern => $set) 
            {
                foreach ($map as $key => $value) 
                {
                    if(!isset($set[$value]) && isset($config[$key]))
                    {
                        $set[$value] = $config[$key];
                    }
                }
                $this->_parsedRules[$module][$pattern] = $this->createRule($set, $pattern);
            }
        }
        return $this->_parsedRules[$module];
    }

    /**
     * Create rule object
     * 
     * @param string $route
     * @param string|array $pattern
     * @param boolean $cache
     * @return eUrlRule
     */
    protected function createRule($route, $pattern = null, $cache = false)
    {
        return new eUrlRule($route, $pattern, $cache);
    }


    /**
     * @param $label
     * @param $val
     * @param $line
     * @return false|void
     */
    private function _debug($label, $val=null, $line=null)
    {
        if(!deftrue('e_DEBUG_SEF'))
        {
            return false;
        }

        e107::getDebug()->log("<h3>SEF: ".$label . " <small>".basename(__FILE__)." (".$line.")</small></h3>".print_a($val,true));
    }

    /**
     * Route current request
     * @param eRequest $request
     * @param bool $checkOnly
     * @return boolean
     */
    public function route(eRequest $request, $checkOnly = false)
    {
        $request->routed = false;
        
        if(isset($_GET[$this->routeVar]))
        {
            $rawPathInfo = $_GET[$this->routeVar];
            unset($_GET[$this->routeVar]);
            $this->_urlFormat = self::FORMAT_GET;
        }
        else 
        {
            $rawPathInfo = rawurldecode($request->getPathInfo());
            //$this->_urlFormat = self::FORMAT_PATH;
        }



        // Ignore social trackers when determining route.
        $get = eHelper::removeTrackers($_GET);

        // Route to front page - index/index/index route
        if(!$rawPathInfo && (!$this->getMainModule() || empty($get)))
        {
            // front page settings will be detected and front page will be rendered
            $request->setRoute('index/index/index');
            $request->addRouteHistory($rawPathInfo);
            $request->routed = true;
            return true;
        }

        // max number of parts is actually 4 - module/controller/action/[additional/pathinfo/vars], here for reference only
        $parts = $rawPathInfo ? explode('/', $rawPathInfo, 4) : array();

        $this->_debug('parts',$parts,  __LINE__);

        // find module - check aliases
        $module = $this->retrieveModule($parts[0]);
        $mainSwitch = false;
        
        // no module found, switch to Main module (pref) if available
        if(null === $module && $this->getMainModule() && $this->isModule($this->getMainModule()))
        {
            $module = $this->getMainModule();
            $rawPathInfo = $module.'/'.$rawPathInfo;
            array_unshift($parts, $module);
            $mainSwitch = true;
        }
        
        $request->routePathInfo = $rawPathInfo;

        $this->_debug('module',$module,  __LINE__);
        $this->_debug('rawPathInfo',$rawPathInfo,  __LINE__);

        
        // valid module
        if(null !== $module)
        {
            // we have valid module
            $config = $this->getConfig($module);

            $this->_debug('config',$module,  __LINE__);
            
            // set legacy state
            eFront::isLegacy(varset($config['legacy']));
            
            // Don't allow single entry if required by module config
            if(!empty($config['noSingleEntry']))
            {
                $request->routed = true;
                if(!eFront::isLegacy())
                {
                    $request->setRoute($this->notFoundRoute); 
                    return false;
                }
                // legacy entry point - include it later in the bootstrap, legacy query string will be set to current
                $request->addRouteHistory($rawPathInfo);
                return true;
            }
            
            // URL format - the one set by current config overrides the auto-detection
            $format = isset($config['format']) && $config['format'] ? $config['format'] : $this->getUrlFormat();
            
            //remove leading module, unnecessary overhead while matching
            array_shift($parts);
            $rawPathInfo = $parts ? implode('/', $parts) : '';
            $pathInfo = $this->removeUrlSuffix($rawPathInfo, $this->urlSuffix);
            
            // retrieve rules if any and if needed
            $rules = $format == self::FORMAT_PATH ? $this->getRules($module) : array();
            
            // Further parsing may still be needed
            if(empty($rawPathInfo))
            {
                $rawPathInfo = $pathInfo;
            }
            
            // parse callback
            if(!empty($config['selfParse']))
            {
                // controller/action[/additional/parms]
                if(!empty($config['urlSuffix'])) $rawPathInfo = $this->removeUrlSuffix($rawPathInfo, $config['urlSuffix']);
                $route = $this->configCallback($module, 'parse', array($rawPathInfo, $_GET, $request, $this, $config), $config['location']);
            }
            // default module route
            elseif($format == self::FORMAT_GET || !$rules)
            {
                $route = $pathInfo;
            }
            // rules available - try to match an Url Rule
            elseif($rules)
            {
            //    $this->_debug('rules',$rules,  __LINE__);

                foreach ($rules as $rule) 
                {
                    $route = $rule->parseUrl($this, $request, $pathInfo, $rawPathInfo);



                    if($route !== false)
                    {
                        eFront::isLegacy($rule->legacy); // legacy include override

                        $this->_debug('rule->legacy',$rule->legacy,  __LINE__);
                        $this->_debug('rule->parseCallback',$rule->parseCallback,  __LINE__);

                        if($rule->parseCallback)
                        {
                            $this->configCallback($module, $rule->parseCallback, array($request), $config['location']);
                        }

                        // parse legacy query string if any
                        $this->_debug('rule->legacyQuery',$rule->legacyQuery,  __LINE__);

                        if(null !== $rule->legacyQuery)
                        {
                            $obj = eDispatcher::getConfigObject($module, $config['location']);
                            // eUrlConfig::legacyQueryString set as legacy string by default in eUrlConfig::legacy() method
                            $vars = new e_vars($request->getRequestParams());
                            $vars->module = $module;
                            $vars->controller = $request->getController();
                            $vars->action = $request->getAction();
                            if($rule->allowVars)
                            {
                                foreach ($rule->allowVars as $key) 
                                {
                                    if(isset($_GET[$key]) && !$request->isRequestParam($key))
                                    {
                                        // sanitize
                                        $vars->$key = preg_replace('/[^\w\-]/', '', $_GET[$key]);
                                    }
                                }
                            }
                            $obj->legacyQueryString = e107::getParser()->simpleParse($rule->legacyQuery, $vars, '0');

                            $this->_debug('obj->legacyQueryString',$obj->legacyQueryString,  __LINE__);
                            unset($vars, $obj);
                        }
                        break;
                    }
                }
            }
            
            // append module to be registered in the request object
            if(false !== $route)
            {
                // don't modify if true - request directly modified by config callback
                if(!$request->routed) 
                {
                    if(eFront::isLegacy()) $this->configCallback($module, 'legacy', array($route, $request), $config['location']);
                    $route = $module.'/'.$route;
                }
            }
            // No route found, we didn't switched to main module auto-magically
            elseif(!$mainSwitch && vartrue($config['errorRoute']))
            {
                $route = !$checkOnly ? $module.'/'.$config['errorRoute'] : false;
            }

        }

        // final fallback
        if(!vartrue($route))
        {
            if($request->routed)
            {
                $route = $request->getRoute();
            }
            
            if(!$route)
            {
                $route = $this->notFoundRoute;
                eFront::isLegacy(''); // reset legacy - not found route isn't legacy call
                $request->routed = true;
                if($checkOnly) return false;
                ## Global redirect on error option
                if(e107::getPref('url_error_redirect', false) && $this->notFoundUrl)
                {
                    $redirect = $this->assemble($this->notFoundUrl, '', 'encode=0&full=1');
                    //echo $redirect; exit;
                    e107::getRedirect()->redirect($redirect, true, 404);
                }
            }
        }

        $this->_debug('route',$route,  __LINE__);

        $request->setRoute($route);
        $request->addRouteHistory($route);
        $request->routed = true;
        return true;
    }

    /**
     * And more BC
     * Checks and does some addtional logic if registered module is of type legacy
     * @param eRequest $request
     * @return void
     */
    public function checkLegacy(eRequest $request)
    {
        $module = $request->getModule();

        // forward from controller to a legacy module - bad stuff
        if(!$request->isDispatched() && $this->getConfigValue($module, 'legacy'))
        {
            eFront::isLegacy($this->getConfigValue($module, 'legacy'));
            
            $url = $this->assemble($request->getRoute(), $request->getRequestParams());
            $request->setRequestInfo($url)->setPathInfo(null)->setRoute(null);

            $_GET = $request->getRequestParams();
            $_SERVER['QUERY_STRING'] = http_build_query($request->getRequestParams(), null, '&');
            
            // Infinite loop impossible, as dispatcher will break because of the registered legacy path
            $this->route($request);
        }
    }

    /**
     * Convenient way to call config methods
     */
    public function configCallback($module, $callBack, $params, $location)
    {
        if(null == $location) $location = eDispatcher::getModuleConfigLocation($module);
        if(!$module || !($obj = eDispatcher::getConfigObject($module, $location))) return false;
        
        return call_user_func_array(array($obj, $callBack), $params);
    }
    
    /**
     * Convert assembled url to shortcode
     * 
     * @param string $route
     * @param array $params
     * @param array $options {@see eRouter::$_defaultAssembleOptions}
     */
    public function assembleSc($route, $params = array(), $options = array())
    {
        //if(is_string($options)) parse_str($options, $options);
        $url = $this->assemble($route, $params, $options);
        return e107::getParser()->createConstants($url, 'mix');
    }
    
    /**
     * Assemble system URL 
     * Examples:
     * <?php
     * $router->assemble('/'); // index page URL e.g. / or /site_folder/
     * $router->assemble('news/view/item?id=1'); // depends on current news config, possible return value is /news/1
     * $router->assemble('*', 'id=1'); // use current request info - /module/controller/action?id=1
     * $router->assemble('* /* /newaction'); // (NO EMPTY SPACES) change only current action - /module/controller/newaction
     * $newsItem = array('news_id' => 1, 'news_sef' => 'My Title', ...); // as retrieved from DB
     * $router->assemble('news/view/item', $newsItem); // All unused key=>values will be removed and NOT appended as GET vars
     * 
     * @param string $route
     * @param array $params
     * @param array $options {@see eRouter::$_defaultAssembleOptions}
     */
    public function assemble($route, $params = array(), $options = array())
    {
        // TODO - url options
        $request = eFront::instance()->getRequest();
        if(is_string($options)) parse_str($options, $options);
        $options = array_merge($this->_defaultAssembleOptions, $options);
        $base = ($options['full'] ? SITEURLBASE : '').$request->getBasePath();

        $anc = '';
        
        
        if(is_string($params)) parse_str($params, $params);
        if(isset($params['#']))
        {
            $anc = '#'.$params['#'];
            unset($params['#']);
        }
        
        // Config independent - Deny parameter keys, useful for directly denying sensitive data e.g. password db fields
        if(isset($options['deny']))
        {
            $list = array_map('trim', explode(',', $options['deny']));
            foreach ($list as $value) 
            {
                unset($params[$value]);
            }
            unset($list);
        }
        
        // Config independent - allow parameter keys, useful to directly allow data (and not to rely on config allowVars) e.g. when retrieved from db
        if(isset($options['allow']))
        {
            $list = array_map('trim', explode(',', $options['allow']));
            $_params = $params;
            $params = array();
            foreach ($list as $value) 
            {
                if(isset($_params[$value])) $params[$value] = $_params[$value];
            }
            unset($list, $_params);
        }
        
        # Optional convenient masks for creating system URL's
        if($route === '/' || empty($route))
        {
            if($params) 
            {
                $params = $this->createPathInfo($params, $options);
                return $base.'?'.$params;
            }
            return $base;
        }
        elseif(strpos($route, '?') !== false)
        {
            $tmp = explode('?', $route, 2);
            $route = $tmp[0];
            parse_str($tmp[1], $params);
            unset($tmp);
        }
        
        if($route === '*')
        {
            $route = $route = explode('/', $request->getRoute());
        }
        elseif(strpos($route, '*') !== false)
        {
            $route = explode('/', $route, 3);
            if($route[0] === '*') $route[0] = $request->getModule();
            if(isset($route[1]) && $route[1] === '*') $route[1] = $request->getController();
        }
        else 
        {
            $route = explode('/', $route, 3);
        }
        
        // we don't know anything about this route, just build it blind
        if(!$this->isModule($route[0]))
        {
            if($params) 
            {
                $params = $this->createPathInfo($params, $options);
                return $base.implode('/', $route).'?'.$params;
            }
            return $base.implode('/', $route);
        }
        
        # fill in index when needed - XXX not needed, may be removed soon
        switch (count($route)) 
        {
            case 1:
                $route[1] = 'index';
                $route[2] = 'index';
            break;
            case 2:
                $route[2] = 'index';
            break;
        }
        
        # aliases
        $module = $route[0];
        $config = $this->getConfig($module);

        $alias = $this->hasAlias($module, vartrue($options['lan'], null)) ? $this->getAliasFromModule($module, vartrue($options['lan'], null)) : $module;
        $route[0] = $alias;
        if($options['encode']) $alias = rawurlencode($alias);
        
        $format = isset($config['format']) && $config['format'] ? $config['format'] : self::FORMAT_GET;
        
        $urlSuffix = '';
        
        // Fix base url for legacy links
        if(!empty($config['noSingleEntry'])) $base = $options['full'] ? SITEURL : e_HTTP;
        elseif(self::FORMAT_GET !== $config['format'])
        {
            $urlSuffix = $this->urlSuffix;
            if(isset($config['urlSuffix'])) $urlSuffix = $config['urlSuffix'];
        } 
        
        // Create by config callback
        if(!empty($config['selfCreate']))
        {
            $tmp = $this->configCallback($module, 'create', array(array($route[1], $route[2]), $params, $options), $config['location']); 
            
            if(empty($tmp)) return '#not-found';
            
            if(is_array($tmp))
            {
                $route = $tmp[0];
                $params = $tmp[1];
                
                if($options['encode']) $route = array_map('rawurlencode', $route);
                $route = implode('/', $route);
            
                if(!$route) 
                {
                    $urlSuffix = '';
                    if(!$this->isMainModule($module)) $route = $alias;
                }
                elseif (!$this->isMainModule($module)) 
                {
                    $route = $alias.'/'.$route;
                }
                
            }
            else 
            {    
                // relative url returned
                return $base.$tmp.$anc;
            }    
            unset($tmp);
            
            if($format === self::FORMAT_GET)
            {
                $params[$this->routeVar] = $route;
                $route = '';
            }
            
            if($params) 
            {
                $params = $this->createPathInfo($params, $options);
                return $base.$route.$urlSuffix.'?'.$params.$anc;
            }

            return $base.$route.$urlSuffix.$anc;
        }
        
        
        // System URL create routine
        $rules = $this->getRules($module);
        if($format !== self::FORMAT_GET && !empty($rules))
        {
            foreach ($rules as $k => $rule)
            {
                if (($url = $rule->createUrl($this, array($route[1], $route[2]), $params, $options)) !== false)
                {
                     return $base.rtrim(($this->isMainModule($module) ? '' : $alias.'/').$url, '/').$anc;
                }
            }
        }

        // default - module/controller/action
        if($this->isMainModule($module)) unset($route[0]);
        if($route[2] == 'index')
        {
            unset($route[2]);
            if($route[1] == 'index') unset($route[1]);
        }
        
        # Modify params if required
        if($params) 
        {
            if(!empty($config['mapVars']))
            {
                foreach ($config['mapVars'] as $srcKey => $dstKey)
                {
                    if (isset($params[$srcKey]))
                    {
                        $params[$dstKey] = $params[$srcKey];
                        unset($params[$srcKey]);
                    }
                    else
                    {
                        trigger_error("Missing ".$srcKey." during URL creation in ".$module, E_USER_NOTICE);
                    }
                }    
            }
            
            // false means - no vars are allowed, nothing to preserve here
            if(varset($config['allowVars']) === false) $params = array();
            // default empty array value - try to guess what's allowed - mapVars is the best possible candidate
            elseif(empty($config['allowVars']) && !empty($config['mapVars'])) $params = array_unique(array_values($config['mapVars']));
            // disallow everything but valid URL parameters
            if(!empty($config['allowVars']))
            {
                $copy = $params;
                $params = array();
                foreach ($config['allowVars'] as $key)
                {
                    if(isset($copy[$key]))
                    {
                        $params[$key] = $copy[$key];
                    }
                    else
                    {
                        trigger_error("Missing ".$key." during URL creation in ".$module, E_USER_NOTICE);
                    }
                }
                unset($copy);
            }
            
            if($format === self::FORMAT_GET)
            {
                $urlSuffix = '';
                $copy = $params;
                $params = array();
                $params[$this->routeVar] = implode('/', $route);
                foreach ($copy as $key => $value) 
                {
                    $params[$key] = $value;
                }
                unset($copy);
                $route = array();
            }
            $params = $this->createPathInfo($params, $options);
            $route = implode('/', $route);
            if(!$route || $route == $alias) $urlSuffix = '';
            return $base.$route.$urlSuffix.'?'.$params.$anc;
        }
        $route = implode('/', $route);
        if(!$route || $route == $alias) $urlSuffix = '';
        
        
        return $format === self::FORMAT_GET ? $base.'?'.$this->routeVar.'='.$route.$anc : $base.$route.$urlSuffix.$anc;
    }
    
    /**
     * Alias of assemble()
     */
    public function url($route, $params = array())
    {
        return $this->assemble($route, $params);
    }
    
    /**
     * Creates a path info based on the given parameters.
     * XXX - maybe we can switch to http_build_query(), should be able to do everything we need in a much better way
     * 
     * @param array $params list of GET parameters
     * @param array $options rawurlencode, equal, encode and amp settings
     * @param string $key this is used internally for recursive calls
     *
     * @return string the created path info
     */
    public function createPathInfo($params, $options, $key = null)
    {
        $pairs = array();
        $equal = $options['equal'];
        $encode = $options['encode'];
        $ampersand = !$encode && $options['amp'] == '&amp;' ? '&' : $options['amp'];
        foreach ($params as $k => $v)
        {
            if (null !== $key) $k = $key.'['.rawurlencode($k).']';

            if (is_array($v)) $pairs[] = $this->createPathInfo($v, $options, $k);
            else 
            {
                if(null === $v)
                {
                    if($encode)
                    {
                        $k = null !== $key ? $k : rawurlencode($k);
                    }
                    $pairs[] = $k;
                    continue;
                }
                if($encode)
                {
                    $k =  null !== $key ? $k : rawurlencode($k);
                    $v = rawurlencode($v);
                }
                $pairs[] = $k.$equal.$v;
            }
        }
        return implode($ampersand, $pairs);
    }

    /**
     * Parses a path info into URL segments
     * Be sure to not use non-unique chars for equal and ampersand signs, or you'll break your URLs
     *
     * @param string $pathInfo path info
     * @param string $equal
     * @param string $ampersand
     * @return array|void
     */
    public function parsePathInfo($pathInfo, $equal = '/', $ampersand = '/')
    {
        if ('' === $pathInfo) return;
        
        if ($equal != $ampersand) $pathInfo = str_replace($equal, $ampersand, $pathInfo);
    //    $segs = explode($ampersand, $pathInfo.$ampersand);
        
        $segs = explode('/', $pathInfo);
        $ret = array();
        
        for ($i = 0, $n = count($segs); $i < $n - 1; $i += 2)
        {
            $key = $segs[$i];
            if ('' === $key) continue;
            $value = $segs[$i + 1]; 
            // array support
            if (($pos = strpos($key, '[')) !== false && ($pos2 = strpos($key, ']', $pos + 1)) !== false)
            {
                $name = substr($key, 0, $pos);
                // numerical array
                if ($pos2 === $pos + 1)
                    $ret[$name][] = $value;
                // associative array
                else
                {
                    $key = substr($key, $pos + 1, $pos2 - $pos - 1);
                    $ret[$name][$key] = $value;
                }
            }
            else 
            {
                $ret[$key] = $value;
                
            }
        }
        return $ret;
    }
    
    /**
     * Removes the URL suffix from path info.
     * @param string $pathInfo path info part in the URL
     * @param string $urlSuffix the URL suffix to be removed
     *
     * @return string path info with URL suffix removed.
     */
    public function removeUrlSuffix($pathInfo, $urlSuffix)
    {
        if ('' !== $urlSuffix && substr($pathInfo, -strlen($urlSuffix)) === $urlSuffix) return substr($pathInfo, 0, -strlen($urlSuffix));
        else return $pathInfo;
    }
}


/**
 *
 */
class eException extends Exception
{
    
}

/**
 * Based on Yii Framework UrlRule handler <www.yiiframework.com>
 */
class eUrlRule
{
    /**
     *
     * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
     * Defaults to null, meaning using the value of {@link cl_shop_core_url::urlSuffix}.
     *
     * @var string the URL suffix used for this rule.
     */
    public $urlSuffix;

    /**
     * When this rule is used to parse the incoming request, the values declared in this property
     * will be injected into $_GET.
     *
     * @var array the default GET parameters (name=>value) that this rule provides.
     */
    public $defaultParams = array();

    /**
     * @var string module/controller/action
     */
    public $route;

    /**
     * @var array the mapping from route param name to token name (e.g. _r1=><1>)
     */
    public $references = array();

    /**
     * @var string the pattern used to match route
     */
    public $routePattern;

    /**
     * @var string regular expression used to parse a URL
     */
    public $pattern;

    /**
     * @var string template used to construct a URL
     */
    public $template;

    /**
     * @var array list of parameters (name=>regular expression)
     */
    public $params = array();

    /**
     * @var boolean whether the URL allows additional parameters at the end of the path info.
     */
    public $append;
    
    /**
     * @var array list of SourceKey=>DestinationKey associations
     */
    public $mapVars = array();
    
    /**
     * Numerical array of allowed parameter keys. If set, everything else will be wiped out from the passed parameter array
     * @var array
     */
    public $allowVars = array();
    
    /**
     * Should be values matched vs route patterns when assembling URLs
     * Warning SLOW when true!!!
     * @var mixed true or 1 for preg_match (extremely slower), or 'empty' for only empty check (better) 
     */
    public $matchValue;
    
    /**
     * Method member of module config object, to be called after successful request parsing
     * @var string
     */
    public $parseCallback;
    
    /**
     * Shortcode path to the old entry point e.g. '{e_BASE}news.php'
     * @var string
     */
    public $legacy;
    
    /**
     * Template used for automated recognition of legacy QueryString (parsed via simpleParser with values of retrieved requestParameters)
     * @var string
     */
    public $legacyQuery;
    
    /**
     * Core regex templates
     * Example usage - route <var:{number}> will result in 
     * @var array
     */
    public $regexTemplates = array(
        'az'                    => '[A-Za-z]+', // NOTE - it won't match non-latin word characters!
        'alphanum'              => '[\w\pL]+',
        'sefsecure'             => '[\w\pL\s\-+.,]+',
        'secure'                 => '[^\/\'"\\<%]+',
        'number'                 => '[\d]+',
        'username'                 => '[\w\pL.\-\s!,]+', // TODO - should equal to username pattern, sync it
        'azOptional'            => '[A-Za-z]{0,}',
        'alphanumOptional'      => '[\w\pL]{0,}',
        'sefsecureOptional'     => '[\w\pL\s\-+.,]{0,}',
        'secureOptional'         => '[^\/\'"\\<%]{0,}',
        'numberOptional'         => '[\d]{0,}',
        'usernameOptional'         => '[\w\pL.\-\s!,]{0,}', // TODO - should equal to username pattern, sync it
    );
    
    /**
     * User defined regex templates
     * @var array
     */
    public $varTemplates = array(); 
    
    /**
     * All regex templates
     * @var e_var
     */
    protected $_regexTemplates;
    

    /**
     * Constructor.
     * @param string $route the route of the URL (controller/action)
     * @param string $pattern the pattern for matching the URL
     */
    public function __construct($route, $pattern, $fromCache = false)
    {
        if (is_array($route))
        {
            if ($fromCache && !$pattern)
            {
                $this->setData($route);
                $this->_regexTemplates = new e_vars($this->regexTemplates);
                return;
            }
            
            $this->setData($route);
            if($this->defaultParams && is_string($this->defaultParams))
            {
                parse_str($this->defaultParams, $this->defaultParams);
            }
            $route = $this->route = $route[0];
        }
        else $this->route = $route;

        $tr2['/'] = $tr['/'] = '\\/';

        if (strpos($route, '<') !== false && preg_match_all('/<(\w+)>/', $route, $matches2))
        {
            foreach ($matches2[1] as $name) $this->references[$name] = "<$name>";
        }
        
        if($this->varTemplates)
        {
            // don't override core regex templates
            $this->regexTemplates = array_merge($this->varTemplates, $this->regexTemplates); 
            $this->varTemplates = array();
        }
        $this->_regexTemplates = new e_vars($this->regexTemplates);

        if (preg_match_all('/<(\w+):?(.*?)?>/', $pattern, $matches))
        {
            $tokens = array_combine($matches[1], $matches[2]);
            $tp = e107::getParser();
            foreach ($tokens as $name => $value)
            {
                if ($value === '') $value = '[^\/]+';
                elseif($value[0] == '{')
                {
                    $value = $tp->simpleParse($value, $this->_regexTemplates, '[^\/]+');
                }
                $tr["<$name>"] = "(?P<$name>$value)";
                if (isset($this->references[$name])) $tr2["<$name>"] = $tr["<$name>"];
                else $this->params[$name] = $value;
            }
        }
        
        $p = rtrim($pattern, '*');
        $this->append = $p !== $pattern;
        $p = trim($p, '/');
        $this->template = preg_replace('/<(\w+):?.*?>/', '<$1>', $p);
        $this->pattern = '/^'.strtr($this->template, $tr).'\/?';
        if ($this->append) $this->pattern .= '/u';
        else $this->pattern .= '$/u';

        if ($this->references !== array()) $this->routePattern = '/^'.strtr($this->route, $tr2).'$/u';
    }

    /**
     * @return array
     */
    public function getData()
    {
        $vars = array_keys(get_class_vars(__CLASS__));
        $data = array();
        foreach ($vars as $prop)
        {
            $data[$prop] = $this->$prop;
        }
        return $data;
    }

    /**
     * @param $data
     * @return void
     */
    protected function setData($data)
    {
        if (!is_array($data)) return;
        $vars = array_keys(get_class_vars(__CLASS__));
        
        foreach ($vars as $prop)
        {
            if (!isset($data[$prop])) continue;
            $this->$prop = $data[$prop];
        }
    }

    /**
     * Creates a URL based on this rule.
     * TODO - more clear logic and flexibility by building the query string
     * 
     * @param eRouter $manager the router/manager
     * @param string $route the route
     * @param array $params list of parameters
     * @param array $options
     * @return false|string|null the constructed URL or false on error
     */
    public function createUrl($manager, $route, $params, $options)
    {
        $case = 'i';
        $ampersand = $options['amp'];
        $encode = vartrue($options['encode']);
        
        if(is_array($route)) $route = implode('/', $route);
        

        
        $tr = array();
        if ($route !== $this->route)
        {
            if ($this->routePattern !== null && preg_match($this->routePattern.$case, $route, $matches))
            {
                foreach ($this->references as $key => $name) $tr[$name] = $matches[$key];
            }
            else return false;
        }
        
        // map vars first
        foreach ($this->mapVars as $srcKey => $dstKey)
        {
            if (isset($params[$srcKey])/* && !isset($params[$dstKey])*/)
            {
                $params[$dstKey] = $params[$srcKey];
                unset($params[$srcKey]);
            }
        }    
            
        // false means - no vars are allowed, preserve only route vars
        if($this->allowVars === false) $this->allowVars = array_keys($this->params);
        // empty array (default) - everything is allowed
        
        // disallow everything but valid URL parameters
        if(!empty($this->allowVars))
        {
            $copy = $params;
            $params = array();
            $this->allowVars = array_unique(array_merge($this->allowVars, array_keys($this->params)));
            foreach ($this->allowVars as $key)
            {
                if(isset($copy[$key])) $params[$key] = $copy[$key];
            }
            unset($copy);
        }

        foreach ($this->defaultParams as $key => $value)
        {
            if (isset($params[$key]))
            {
                if ($params[$key] == $value) unset($params[$key]);
                else return false;
            }
        }
        
        foreach ($this->params as $key => $value) if (!isset($params[$key])) return false;
        
        if($this->matchValue)
        {
            
            if('empty' !== $this->matchValue)
            {
                foreach($this->params as $key=>$value)
                {
                    if(!preg_match('/'.$value.'/'.$case,$params[$key]))
                        return false;
                }
            }
            else
            {
                foreach($this->params as $key=>$value)
                {
                    if(empty($params[$key]) )
                        return false;
                }
            }
        }

        $tp = e107::getParser();
        $urlFormat = e107::getConfig()->get('url_sef_translate');

        foreach ($this->params as $key => $value)
        {
            // FIX - non-latin URLs proper encoded
            $tr["<$key>"] = rawurlencode($params[$key]); //todo transliterate non-latin
        //    $tr["<$key>"] = eHelper::title2sef($tp->toASCII($params[$key]), $urlFormat); // enabled to test.
            unset($params[$key]);
        }
        
        $suffix = $this->urlSuffix === null ? $manager->urlSuffix : $this->urlSuffix;
        
        // 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)



        
        if($urlFormat == 'dashl' || $urlFormat == 'underscorel' || $urlFormat == 'plusl') // convert template to lowercase when using lowercase SEF URL format.  
        {
            $this->template = strtolower($this->template);    
        }
        
        $url = strtr($this->template, $tr);

        // Work-around fix for lowercase username
        if($urlFormat == 'dashl' && $this->route == 'profile/view')
        {
            $url = str_replace('%20','-', strtolower($url));
        }

        if(empty($params))
        {
             return $url !== '' ? $url.$suffix : $url;
        }

        // apppend not supported, maybe in the future...?
        if ($this->append) $url .= '/'.$manager->createPathInfo($params, '/', '/').$suffix;
        else
        {
            if ($url !== '') $url = $url.$suffix;

            $options['equal'] = '=';
            $url .= '?'.$manager->createPathInfo($params, $options);
        }
    

        return rtrim($url, '/');
    }

    /**
     * Parases a URL based on this rule.
     * @param eRouter $manager the router/URL manager
     * @param eRequest $request the request object
     * @param string $pathInfo path info part of the URL
     * @param string $rawPathInfo path info that contains the potential URL suffix
     * @return mixed the route that consists of the controller ID and action ID or false on error
     */
    public function parseUrl($manager, $request, $pathInfo, $rawPathInfo)
    {
        $case = 'i';    # 'i' = insensitive
        
        if ($this->urlSuffix !== null)    $pathInfo = $manager->removeUrlSuffix($rawPathInfo, $this->urlSuffix);
        
        $pathInfo = rtrim($pathInfo, '/').'/';
        // pathInfo is decoded, pattern could be encoded - required for proper url assemble (e.g. cyrillic chars)
        if (preg_match(rawurldecode($this->pattern).$case, $pathInfo, $matches))
        {
            foreach ($this->defaultParams as $name => $value)
            {
                //if (!isset($_GET[$name])) $_REQUEST[$name] = $_GET[$name] = $value;
                if (!$request->isRequestParam($name)) $request->setRequestParam($name, $value);
            }
            $tr = array();
            foreach ($matches as $key => $value)
            {
                if (isset($this->references[$key])) $tr[$this->references[$key]] = $value;
                elseif (isset($this->params[$key])) 
                {
                    //$_REQUEST[$key] = $_GET[$key] = $value;
                    $request->setRequestParam($key, $value);
                }
            }
            
            if ($pathInfo !== $matches[0])    # Additional GET params exist
            {
                $manager->parsePathInfo($request, ltrim(substr($pathInfo, strlen($matches[0])), '/'));
            }
            return (null !== $this->routePattern ? strtr($this->route, $tr) : $this->route);
        }
        else return false;
    }
    
}


/**
 *
 */
abstract class eUrlConfig
{
    /**
     * Registered by parse method legacy query string
     */
    public $legacyQueryString = null;
    
    /**
     * User defined initialization
     */
    public function init() {}
    
    /**
     * Retrieve module config options (including url rules if any)
     * Return array is called once and cached, so runtime changes are not an option
     * @return array
     */
    abstract public function config();
    
    /**
     * Create URL callback, called only when config option selfParse is set to true
     * Expected return array format:
     * <code>
     * array(
     *     array(part1, part2, part3),
     *     array(parm1 => val1, parm2 => val2),
     * );
     * </code>
     * @param array $route parts
     * @param array $params
     * @return array|string numerical of type (routeParts, GET Params)| string route or false on error
     */
    public function create($route, $params = array(), $options = array()) {}

    /**
     * Parse URL callback, called only when config option selfCreate is set to true
     * TODO - register variable eURLConfig::currentConfig while initializing the object, remove from method arguments
     * @param string $pathInfo
     * @param array $params request parameters
     * @param eRequest|null $request
     * @param eRouter|null $router
     * @param array $config
     * @return string route or false on error
     */
    public function parse($pathInfo, $params = array(), eRequest $request = null, eRouter $router = null, $config = array())
    {
        return false;
    }
    
    /**
     * Legacy callback, used called when config option legacy is not empty
     * By default it sets legacy query string to $legacyQueryString value (normaly assigned inside of the parse method)
     * @param string $resolvedRoute
     * @param eRequest $request
     * @param string $callType 'route' - called once, when parsing the request, 'dispatch' - called inside the dispatch loop (in case of controller _forward)
     * @param void
     */
    public function legacy($resolvedRoute, eRequest $request, $callType = 'route') 
    {
        if($this->legacyQueryString !== null) 
        {
            $request->setLegacyQstring($this->legacyQueryString);
            $request->setLegacyPage();
        }
    }
    
    /**
     * Developed mainly for legacy modules.
     * It should be manually triggered inside of old entry point. The idea is 
     * to avoid multiple URL addresses having same content (bad SEO practice)
     * FIXME - under construction
     */
    public function forward() {}
    
    /**
     * Admin interface callback, returns array with all required from administration data
     * Return array structure:
     * <code>
     * <?php
     * return array(
     *   'labels' => array(
     *       'name' => 'Module name',
     *         'label' => 'Profile Label',
     *         'description' => 'Additional profile info, exmples etc.',
     *      ),
     *      'form' => array(), // awaiting future development
     *      'callbacks' => array(), // awaiting future development
     * );
     * </code>
     */
    public function admin() { return array(); }
    
    /**
     * Admin submit hook
     * FIXME - under construction
     */
    public function submit() {}
    
    /**
     * Admin interface help messages, labels and titles
     * FIXME - under construction
     */
    public function help() {}
    
    
}

/**
 * Controller base class, actions are extending it
 * 
 */
class eController
{
    protected $_request;
    protected $_response;

    /**
     * @param eRequest $request
     * @param eResponse|null $response
     */
    public function __construct(eRequest $request, eResponse $response = null)
    {
        $this->setRequest($request)
            ->setResponse($response)
            ->init();
    }
    
    /**
     * Custom init, always called in the constructor, no matter what is the request dispatch status
     */
    public function init() {}
    
    /**
     * Custom shutdown, always called after the controller dispatch, no matter what is the request dispatch status
     */
    public function shutdown() {}
    
    /**
     * Pre-action callback, fired only if dispatch status is still true and action method is found
     */
    public function preAction() {}
    
    /**
     * Post-action callback, fired only if dispatch status is still true and action method is found
     */
    public function postAction() {}
    
    /**
     * @param eRequest $request
     * @return eController
     */
    public function setRequest($request)
    {
        $this->_request = $request;
        return $this;
    }
    
    /**
     * @return eRequest
     */
    public function getRequest()
    {
        return $this->_request;
    }
    
    /**
     * @param eResponse $response
     * @return eController
     */
    public function setResponse($response)
    {
        $this->_response = $response;
        return $this;
    }
    
    /**
     * @return eResponse
     */
    public function getResponse()
    {
        return $this->_response;
    }

    /**
     * @param $content
     * @return $this
     */
    public function addBody($content)
    {
        $this->getResponse()->appendBody($content);
        return $this;
    }

    /**
     * @param $description
     * @return $this
     */
    public function addMetaDescription($description)
    {
        $this->getResponse()->addMetaDescription($description);
        return $this;
    }
    
    /**
     * Add document title
     * @param string $title
     * @param boolean $meta auto-add it as meta-title
     * @return eController
     */
    public function addTitle($title, $meta = true)
    {
        $this->getResponse()->appendTitle($title);
        if($meta) $this->addMetaTitle(strip_tags($title));
        return $this;
    }


    /**
     * @param $title
     * @return $this
     */
    public function addMetaTitle($title)
    {
        $this->getResponse()->addMetaTitle($title);
        return $this;
    }

    /**
     * @param $actionMethodName
     * @return void
     * @throws eException
     */
    public function dispatch($actionMethodName)
    {
        $request = $this->getRequest();
        $content = '';
        
        // init() could modify the dispatch status
        if($request->isDispatched())
        {        
            if(method_exists($this, $actionMethodName)) 
            {
                $this->preAction();
                // TODO request userParams() to store private data - check for noPopulate param here
                if($request->isDispatched())
                {
                    $request->populateRequestParams();
                    
                    // allow return output
                    $content = $this->$actionMethodName();
                    if(!empty($content)) $this->addBody($content);
                    
                    if($request->isDispatched())
                    {
                        $this->postAction();
                    }
                }
            }
            else 
            {
                //TODO not found method by controller or default one
                $action = substr($actionMethodName, 6);
                throw new eException('Action "'.$action.'" does not exist');
            }
        }
        $this->shutdown();
    }

    /**
     * @param eRequest|null $request
     * @param eResponse|null $response
     * @return eResponse
     * @throws eException
     */
    public function run(eRequest $request = null, eResponse $response = null)
    {
        if(null === $request) $request = $this->getRequest();
        else $this->setRequest($request);
        
        if(null === $response) $response = $this->getResponse();
        else $this->setResponse($response);
        
        $action = $request->getActionMethodName();
        
        $request->setDispatched(true);
        $this->dispatch($action);
        
        return $this->getResponse();
    }

    /**
     * @param $url
     * @param $createURL
     * @param $code
     * @return void
     */
    protected function _redirect($url, $createURL = false, $code = null)
    {
        $redirect = e107::getRedirect();
        if($createURL)
        {
            $url = eFront::instance()->getRouter()->assemble($url, '', 'encode=0');
        }
        if(strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0)
        {
            $url = $url[0] == '/' ? SITEURLBASE.$url : SITEURL.$url;
        }
        $redirect->redirect($url, true, $code);
    }
    
    /**
     * System forward
     * @param string $route
     * @param array $params
     */
    protected function _forward($route, $params = array())
    {
        $request = $this->getRequest();
        
        if(is_string($params))
        {
            parse_str($params, $params);
        }
        
        $oldRoute = $request->getRoute();
        $route = explode('/', trim($route, '/'));
        
        switch (count($route)) {
            case 3:
                if($route[0] !== '*') $request->setModule($route[0]);
                if($route[1] !== '*') $request->setController($route[1]);
                $request->setAction($route[2]);
            break;
            
            case 2:
                if($route[1] !== '*') $request->setController($route[0]);
                $request->setAction($route[1]);
            break;
            
            case 1:
                $request->setAction($route[0]);
            break;
            
            default:
                return;
            break;
        }
        
        $request->addRouteHistory($oldRoute);
        
        if(false !== $params) $request->setRequestParams($params);
        $request->setDispatched(false);
    }
    
    /**
     * @param  string $methodName
     * @param  array $args
     * @return void
     * @throws eException
     */
    public function __call($methodName, $args)
    {
        if (strpos($methodName, 'action') === 0)
        {
            $action = substr($methodName, 6);
            throw new eException('Action "'.$action.'" does not exist', 2404);
        }

        throw new eException('Method "'.$methodName.'" does not exist', 3404);
    }
}

/**
 * @package e107
 * @subpackage e107_handlers
 * @version $Id$
 *
 * Base front-end controller
 */

class eControllerFront extends eController
{
    /**
     * Plugin name - used to check if plugin is installed
     * Set this only if plugin requires installation
     * @var string
     */
    protected $plugin = null;
    
    /**
     * Default controller access
     * @var integer
     */
    protected $userclass = e_UC_PUBLIC;
    
    /**
     * Generic 404 page URL (redirect), SITEURL will be added
     * @var string
     */
    protected $e404 = '404.html';
    
    /**
     * Generic 403 page URL (redirect), SITEURL will be added
     * @var string
     */
    protected $e403 = '403.html';
    
    /**
     * Generic 404 route URL (forward)
     * @var string
     */
    protected $e404route = 'index/not-found';
    
    /**
     * Generic 403 route URL (forward)
     * @var string
     */
    protected $e403route = 'index/access-denied';
    
    /**
     * View renderer objects
     * @var array
     */
    protected $_validator;
    
    /**
     * Per action access
     * Format 'action' => userclass
     * @var array
     */
    protected $access = array();
    
    /**
     * User input filter (_GET)
     * Format 'action' => array(var => validationArray)
     * @var array
     */
    protected $filter = array();
    
    /**
     * Base constructor - set 404/403 locations
     */
    public function __construct(eRequest $request, eResponse $response = null)
    {
        parent::__construct($request, $response);
        $this->_init();
    }
    
    /**
     * Base init, called after the public init() - handle access restrictions
     * The base init() method is able to change controller variables on the fly (e.g. access, filters, etc)
     */
    final protected function _init()
    {
        // plugin check
        if(null !== $this->plugin)
        {
            if(!e107::isInstalled($this->plugin))
            {
                $this->forward403();
                return;
            }
        }
        
        // global controller restriction
        if(!e107::getUser()->checkClass($this->userclass, false))
        {
            $this->forward403();
            return;
        }
        
        // by action access
        if(!$this->checkActionPermissions()) exit;
        
        // _GET input validation
        $this->validateInput();
        
        // Set Render mode to module-controller-action, override possible within the action
        $this->getResponse()->setRenderMod(str_replace('/', '-', $this->getRequest()->getRoute()));
    }
    
    /**
     * Check persmission for current action
     * @return boolean
     */
    protected function checkActionPermissions()
    {
        // per action restrictions
        $action = $this->getRequest()->getAction();
        if(isset($this->access[$action]) && !e107::getUser()->checkClass($this->access[$action], false))
        {
            $this->forward403();
            return false;
        }
        return true;
    }

    /**
     * @return void
     */
    public function redirect404()
    {
        e107::getRedirect()->redirect(SITEURL.$this->e404);
    }

    /**
     * @return void
     */
    public function redirect403()
    {
        e107::getRedirect()->redirect(SITEURL.$this->e403);
    }

    /**
     * @return void
     */
    public function forward404()
    {
        $this->_forward($this->e404route);
    }

    /**
     * @return void
     */
    public function forward403()
    {
        $this->_forward($this->e403route);
    }
    
    /**
     * Controller validator object
     * @return e_validator
     */
    public function getValidator()
    {
        if(null === $this->_validator)
        {
            $this->_validator = new e_validator('controller');
        }
        
        return $this->_validator;
    }
    
    /**
     * Register request parameters based on current $filter data (_GET only)
     * Additional security layer
     */
    public function validateInput()
    {
        $validator = $this->getValidator(); 
        $request = $this->getRequest();
        if(empty($this->filter) || !isset($this->filter[$request->getAction()])) return;
        $validator->setRules($this->filter[$request->getAction()])
            ->validate($_GET);
        
        $validData = $validator->getValidData();
        
        foreach ($validData as $key => $value) 
        {
            if(!$request->isRequestParam($key)) $request->setRequestParam($key, $value);
        }
        $validator->clearValidateMessages();
    }
    
    /**
     * System error message proxy
     * @param string $message
     * @param boolean $session
     */
    public function messageError($message, $session = false)
    {
        return e107::getMessage()->addError($message, 'default', $session);
    }
    
    /**
     * System success message proxy
     * @param string $message
     * @param boolean $session
     */
    public function messageSuccess($message, $session = false)
    {
        return e107::getMessage()->addSuccess($message, 'default', $session);
    }
    
    /**
     * System warning message proxy
     * @param string $message
     * @param boolean $session
     */
    public function messageWarning($message, $session = false)
    {
        return e107::getMessage()->addWarning($message, 'default', $session);
    }
    
    /**
     * System debug message proxy
     * @param string $message
     * @param boolean $session
     */
    public function messageDebug($message, $session = false)
    {
        return e107::getMessage()->addDebug($message, 'default', $session);
    }
}


/**
 * Request handler
 * 
 */
class eRequest
{
    /**
     * @var string
     */
    protected $_module;
    
    /**
     * @var string
     */
    protected $_controller;
    
    /**
     * @var string
     */
    protected $_action;
    
    /**
     * Request status
     * @var boolean
     */
    protected $_dispatched = false;
    
    /**
     * @var array
     */
    protected $_requestParams = array();
    
    /**
     * @var string
     */
    protected $_basePath;
    
    /**
     * @var string
     */
    protected $_pathInfo;
    
    
    /**
     * @var string
     */
    protected $_requestInfo;
    
    /**
     * Pathinfo string used for initial system routing
     */
    public $routePathInfo;
    
    /**
     * @var array
     */
    protected $_routeHistory = array();
    
    /**
     * @var boolean if request is already routed - generally set by callbacks to notify router about route changes
     */
    public $routed = false;
    
    /**
     * Name of the bootstrap file
     * @var string
     */
    public $singleEntry = 'index.php';

    /**
     * Request constructor
     */
    public function __construct($route = null)
    {
        if(null !== $route) 
        {
            $this->setRoute($route);
            $this->routed = true;
        }
    }

    /**
     * Get system base path
     * @return string
     */
    public function getBasePath()
    {
        if(null == $this->_basePath) 
        {
            $this->_basePath = e_HTTP;
            if(!e107::getPref('url_disable_pathinfo')) $this->_basePath .= $this->singleEntry.'/';
        }
        
        return $this->_basePath;
    }
    
    /**
     * Set system base path
     * @param string $basePath
     * @return eRequest
     */
    public function setBasePath($basePath)
    {
        $this->_basePath = $basePath;
        return $this;
    }
    
    /**
     * Get path info
     * If not set, it'll be auto-retrieved
     * @return string path info
     */
    public function getPathInfo()
    {
        if(null == $this->_pathInfo)
        {
            if($this->getBasePath() == $this->getRequestInfo()) 
                $this->_pathInfo = ''; // map to indexRoute
                
            else 
                $this->_pathInfo = substr($this->getRequestInfo(), strlen($this->getBasePath()));
            
            if($this->_pathInfo && trim($this->_pathInfo, '/') == trim($this->singleEntry, '/')) $this->_pathInfo = '';
        }
        
        return $this->_pathInfo;
    }
    
    /**
     * Override path info
     * @param string $pathInfo
     * @return eRequest
     */
    public function setPathInfo($pathInfo)
    {
        $this->_pathInfo = $pathInfo;
        return $this;
    }
    
    /**
     * @return string request info
     */
    public function getRequestInfo()
    {
        if(null === $this->_requestInfo)
        {
            $this->_requestInfo = e_REQUEST_HTTP;
        }
        return $this->_requestInfo;
    }


    /**
     * Override request info
     * @param string $requestInfo
     * @return eRequest
     */
    public function setRequestInfo($requestInfo)
    {
        $this->_requestInfo = $requestInfo;
        return $this;
    }
    
    /**
     * Quick front page check 
     */
    public static function isFrontPage($entryScript = 'index.php', $currentPathInfo = e_REQUEST_HTTP)
    {
        $basePath = e_HTTP;
        if(!e107::getPref('url_disable_pathinfo')) $basePath .= $entryScript.'/';
        
        return ($basePath == $currentPathInfo);
    }
    
    /**
     * Get current controller string
     * @return string
     */
    public function getController()
    {
        return $this->_controller;
    }
    
    /**
     * Get current controller name
     * Example: requested controller-name or 'controller name' -> converted to controller_name
     * @return string
     */
    public function getControllerName()
    {
        return eHelper::underscore($this->_controller);
    }
    
    /**
     * Set current controller name
     * Example: controller_name OR 'controller name' -> converted to controller-name
     * Always sanitized
     * @param string $controller
     * @return eRequest
     */
    public function setController($controller)
    {
        $this->_controller = strtolower(eHelper::dasherize($this->sanitize($controller)));
        return $this;
    }
    
    /**
     * Get current module string
     * @return string
     */
    public function getModule()
    {
        return $this->_module;
    }
    
    /**
     * Get current module name
     * Example: module-name OR 'module name' -> converted to module_name
     * @return string
     */
    public function getModuleName()
    {
        return eHelper::underscore($this->_module);
    }
    
    /**
     * Set current module name
     * Example: module_name OR 'module name' -> converted to module-name
     * Always sanitized
     * @param string $module
     * @return eRequest
     */
    public function setModule($module)
    {
        $this->_module = strtolower(eHelper::dasherize($this->sanitize($module)));
        return $this;
    }
    
    /**
     * Get current action string
     * @return string
     */
    public function getAction()
    {
        return $this->_action;
    }
    
    /**
     * Get current action name
     * Example: action-name OR 'action name' OR action_name -> converted to ActionName
     * @return string
     */
    public function getActionName()
    {
        return eHelper::camelize($this->_action, true);
    }
    
    /**
     * Get current action method name
     * Example: action-name OR 'action name' OR action_name -> converted to actionActionName
     * @return string
     */
    public function getActionMethodName()
    {
        return 'action'.eHelper::camelize($this->_action, true);
    }
    
    /**
     * Set current action name
     * Example: action_name OR 'action name' OR Action_Name OR 'Action Name' -> converted to ation-name
     * Always sanitized
     * @param string $action
     * @return eRequest
     */
    public function setAction($action)
    {
        $this->_action = strtolower(eHelper::dasherize($this->sanitize($action)));
        return $this;
    }
    
    /**
     * Get current route string/array -> module/controller/action
     * @param boolean $array
     * @return string|array route
     */
    public function getRoute($array = false)
    {
        if(!$this->getModule()) 
        {
            $route = array('index', 'index', 'index');
        }    
        else
        {
            $route = array(
                $this->getModule(),
                $this->getController() ? $this->getController() : 'index',
                $this->getAction() ? $this->getAction() : 'index',
            );
        }
        return ($array ? $route : implode('/', $route));
    }
    
    /**
     * Set current route
     * @param string $route module/controller/action
     * @return eRequest
     */
    public function setRoute($route)
    {
        if(null === $route) 
        {
            $this->_module = null;
            $this->_controller = null;
            $this->_action = null;
        }
        return $this->initFromRoute($route);
    }
    
    /**
     * System routing track, used in controllers forwarder
     * @param string $route
     * @return eRequest
     */
    public function addRouteHistory($route)
    {
        $this->_routeHistory[] = $route;
        return $this;
    }
    
    /**
     * Retrieve route from history track
     * Based on $source we can retrieve 
     * - array of all history records
     * - 'first' route record
     * - 'last' route record
     * - history record by its index number
     * @param mixed $source
     * @return string|array
     */
    public function getRouteHistory($source = null)
    {
        if(null === $source) return $this->_routeHistory;
        
        if(!$this->_routeHistory) return null;
        elseif('last' === $source)
        {
            return $this->_routeHistory[count($this->_routeHistory) -1]; 
        }
        elseif('first' === $source)
        {
            return $this->_routeHistory[0]; 
        }
        elseif(is_int($source))
        {
            return isset($this->_routeHistory[$source]) ? $this->_routeHistory[$source] : null;
        }
        return null;
    }
    
    /**
     * Search route history for the given $route
     * 
     * @param string $route
     * @return integer route index or false if not found
     */
    public function findRouteHistory($route)
    {
        return array_search($route, $this->_routeHistory);
    }
    
    /**
     * Populate module, controller and action from route string
     * @param string $route
     * @return eRequest
     */
    public function initFromRoute($route)
    {
        $route = trim($route, '/');
        if(!$route) 
        {
            $route = 'index/index/index';
        }        
        $parts = explode('/', $route);
        $this->setModule($parts[0])
            ->setController(vartrue($parts[1], 'index'))
            ->setAction(vartrue($parts[2], 'index'));
            
        return $this;//->getRoute(true);
    }

    /**
     * Get request parameter
     * @param string $key
     * @param string $default value if key not set
     * @return mixed value
     */
    public function getRequestParam($key, $default = null)
    {
        return (isset($this->_requestParams[$key]) ? $this->_requestParams[$key] : $default);
    }
    
    /**
     * Check if request parameter exists
     * @param string $key
     * @return boolean
     */
    public function isRequestParam($key)
    {
        return isset($this->_requestParams[$key]);
    }
    
    /**
     * Get request parameters array
     * @return array value
     */
    public function getRequestParams()
    {
        return $this->_requestParams;
    }
    
    /**
     * Set request parameter
     * @param string $key
     * @param mixed $value
     * @return eRequest
     */
    public function setRequestParam($key, $value)
    {
        $this->_requestParams[$key] = $value;
        return $this;
    }
    
    /**
     * Set request parameters
     * @param array $params
     * @return eRequest
     */
    public function setRequestParams($params)
    {
        $this->_requestParams = $params;
        return $this;
    }
    
    /**
     * Populate current request parameters (_GET scope)
     * @return eRequest
     */
    public function populateRequestParams()
    {
        $rp = $this->getRequestParams();
        
        foreach ($rp as $key => $value) 
        {
            $_GET[$key] = $value;
        }
        return $this;
    }
    
    /**
     * More BC
     * @param string $qstring
     * @return eRequest
     */
    public function setLegacyQstring($qstring = null)
    {
        if(defined('e_QUERY')) return $this;
        
        if(null === $qstring)
        {
            $qstring = self::getQueryString();
        }

        if(!defined('e_SELF'))
        {
            define("e_SELF", e_REQUEST_SELF);
        }

        if(!defined('e_QUERY'))
        {
            define("e_QUERY", $qstring);
        }

        $_SERVER['QUERY_STRING'] = e_QUERY;    
        
        if(strpos(e_QUERY,"=")!==false ) // Fix for legacyQuery using $_GET ie. ?x=y&z=1 etc. 
        {
            parse_str(str_replace(array('&amp;'), array('&'), e_QUERY),$tmp);
            foreach($tmp as $key=>$value)
            {
                $_GET[$key] = $value;    
            }
        }
        
        return $this;
    }
    
    /**
     * And More BC :/
     * @param string $page
     * @return eRequest
     */
    public function setLegacyPage($page = null)
    {
        if(defined('e_PAGE')) return $this;
        if(null === $page)
        {
            $page = eFront::isLegacy();
        }
        if(!$page) 
        {
            define('e_PAGE', $this->singleEntry);
        }
        else define('e_PAGE', basename(str_replace(array('{', '}'), '/', $page)));
        return $this;
    }
    
    /**
     * And More from the same - BC :/
     * @return string
     */
    public static function getQueryString()
    {
        $qstring = '';
        if($_SERVER['QUERY_STRING'])
        {
            $qstring = str_replace(array('{', '}', '%7B', '%7b', '%7D', '%7d'), '', rawurldecode($_SERVER['QUERY_STRING']));
        }
        $qstring = str_replace('&', '&amp;', e107::getParser()->post_toForm($qstring));
        return $qstring;
    }
    
    /**
     * Basic sanitize method for module, controller and action input values
     * @param string $str string to be sanitized
     * @param string $pattern optional replace pattern
     * @param string $replace optional replace string, defaults to dash
     */
    public function sanitize($str, $pattern='', $replace='-')
    {
        if (!$pattern) $pattern = '/[^\w\pL-]/u';
        
        return preg_replace($pattern, $replace, $str);
    }
    
    /**
     * Set dispatched status of the request
     * @param boolean $mod
     * @return eRequest
     */
    public function setDispatched($mod)
    {
        $this->_dispatched = $mod ? true : false;
        return $this;
    }
    
    /**
     * Get dispatched status of the request
     * @return boolean
     */
    public function isDispatched()
    {
        return $this->_dispatched;
    }
}


/**
 *
 */
class eResponse
{
    protected $_body = array('default' => '');
    protected $_title = array('default' => array());
    protected $_e_PAGETITLE = array(); // partial <title> tag.
    protected $_e_PAGETITLE_OVERRIDE = array(); // Full <title> tag
    protected $_META_DESCRIPTION = array();
    protected $_META_KEYWORDS = array();
    protected $_render_mod = array('default' => 'default');
    protected $_meta_title_separator = ' - ';
    protected $_meta_name_only = array(
                        'keywords', 'viewport', 'robots', 'twitter:url', 'twitter:title', 'twitter:card',
                        'twitter:image', 'twitter:description',
    ); // Keep FB happy.
    protected $_meta_property_only = array(  // Keep FB happy.
                            'article:section', 'article:tag', 'article:published_time', 'article:modified_time',
                            'og:description', 'og:image', 'og:title', 'og:updated_time','og:url', 'og:type'
    );
    protected $_meta_multiple = array();
    protected $_meta = array();
    protected $_meta_robot_types = array('noindex'=>'NoIndex', 'nofollow'=>'NoFollow','noarchive'=>'NoArchive','noimageindex'=>'NoImageIndex' );
    protected $_title_separator = ' &raquo; ';
    protected $_content_type = 'html';
    protected $_content_type_arr =  array(
        'html' => 'text/html',
        'css' => 'text/css',
        'xml' => 'text/xml',
        'json' => 'application/json',
        'js' => 'application/javascript',
        'rss' => 'application/rss+xml',
        'soap' => 'application/soap+xml',
    );

    protected $_params = array(
        'render' => true,
        'meta' => false,
        'jsonNoTitle' => false,
        'jsonRender' => false,
    );

    /**
     * @return string[]
     */
    public function getRobotTypes()
    {
        return $this->_meta_robot_types;
    }

    /**
     * @return array
     */
    public function getRobotDescriptions()
    {
        $_meta_robot_descriptions = array(
            'noindex'       => LAN_ROBOTS_NOINDEX,
            'nofollow'      => LAN_ROBOTS_NOFOLLOW,
            'noarchive'     => LAN_ROBOTS_NOARCHIVE,
            'noimageindex'  => LAN_ROBOTS_NOIMAGE );

        return $_meta_robot_descriptions;
    }

    /**
     * @param $key
     * @param $value
     * @return $this
     */
    public function setParam($key, $value)
    {
        $this->_params[$key] = $value;
        return $this;
    }

    /**
     * @param $params
     * @return $this
     */
    public function setParams($params)
    {
        $this->_params = $params;
        return $this;
    }

    /**
     * @param $key
     * @param $default
     * @return mixed|null
     */
    public function getParam($key, $default = null)
    {
        return (isset($this->_params[$key]) ? $this->_params[$key] : $default);
    }

    /**
     * @param $key
     * @return bool
     */
    public function isParam($key)
    {
        return isset($this->_params[$key]);
    }

    /**
     * @param $typeName
     * @param $mediaType
     * @return $this
     */
    public function addContentType($typeName, $mediaType)
    {
        $this->_content_type_arr[$typeName] = $mediaType;
        return $this;
    }

    /**
     * @return string
     */
    public function getContentType()
    {
        return $this->_content_type;
    }

    /**
     * @param $typeName
     * @return mixed|string|void
     */
    public function getContentMediaType($typeName)
    {
        if(isset($this->_content_type_arr[$typeName]))
            return $this->_content_type_arr[$typeName];
    }

    /**
     * @param $typeName
     * @return void
     */
    public function setContentType($typeName)
    {
        $this->_content_type = $typeName;
    }
    
    /**
     * @return eResponse
     */
    public function sendContentType()
    {
        $ctypeStr = $this->getContentMediaType($this->getContentType());
        if($ctypeStr)
        {
            header('Content-type: '.$this->getContentMediaType($this->getContentType()).'; charset=utf-8', TRUE);
        }
        return $this;
    }
    
    /**
     * @return eResponse
     */
    public function addHeader($header, $override = false, $responseCode = null)
    {
        header($header, $override, $responseCode);
        return $this;
    }
    
    /**
     * Append content
     * @param string $body
     * @param string $ns namespace
     * @return eResponse
     */
    public function appendBody($body, $ns = 'default')
    {
        if(!isset($this->_body[$ns]))
        {
            $this->_body[$ns] = '';
        }
        $this->_body[$ns] .= $body;
        
        return $this;
    }
    
    /**
     * Set content
     * @param string $body
     * @param string $ns namespace
     * @return eResponse
     */
    public function setBody($body, $ns = 'default')
    {
        $this->_body[$ns] = $body;
        return $this;
    }


    /**
     * @param $name
     * @param $content
     * @return $this
     */
    public function setMeta($name, $content)
    {
        foreach($this->_meta as $k=>$v)
        {
            if(!empty($v['name']) && ($v['name'] === $name))
            {
                $this->_meta[$k]['content'] = $content;
            }
        }

        return $this;

    }


    /**
     * Removes a Meta tag by name/property.
     *
     * @param string $name
     *   'name' or 'property' for the meta tag we want to remove.
     *
     * @return eResponse $this
     */
    public function removeMeta($name)
    {
        foreach($this->_meta as $k=>$v)
        {
            // Meta tags like: <meta content="..." name="description" />
            if(isset($v['name']) && $v['name'] === $name)
            {
                unset($this->_meta[$k]);
                continue;
            }

            // Meta tags like: <meta content="..." property="og:title" />
            if(isset($v['property']) && $v['property'] === $name)
            {
                unset($this->_meta[$k]);
            }
        }

        return $this;
    }

    
    /**
     * Prepend content
     * @param string $body
     * @param string $ns namespace
     * @return eResponse
     */
    function prependBody($body, $ns = 'default')
    {
        if(!isset($this->_body[$ns]))
        {
            $this->_body[$ns] = '';
        }
        // $this->_body[$ns] = $content.$this->_body[$ns];
        
        return $this;
    }
    
    /**
     * Get content
     * @param string $ns
     * @param boolean $reset
     * @return string
     */
    public function getBody($ns = 'default', $reset = false)
    {
        if(!isset($this->_body[$ns]))
        {
            $this->_body[$ns] = '';
        }
        $ret = $this->_body[$ns];
        if($reset) unset($this->_body[$ns]);
        
        return $ret;
    }
    
    /**
     * @param string $title
     * @param string $ns
     * @return eResponse
     */
    function setTitle($title, $ns = 'default')
    {

        if(!is_string($ns) || empty($ns))
        {
            $this->_title['default'] = array((string) $title);
        }
        else
        {
            $this->_title[$ns] = array((string) $title);
        }
        return $this;
    }

    /**
     * @param string $title
     * @param string $ns
     * @return eResponse
     */
    function appendTitle($title, $ns = 'default')
    {
        if(empty($title))
        {
            return $this;
        }
        if(!is_string($ns) || empty($ns))
        {
            $ns = 'default';
        }
        elseif(!isset($this->_title[$ns]))
        {
            $this->_title[$ns] = array();
        }
        $this->_title[$ns][] = (string) $title;
        return $this;
    }

    /**
     * @param string $title
     * @param string $ns
     * @return eResponse
     */
    function prependTitle($title, $ns = 'default')
    {
        if(empty($title))
        {
            return $this;
        }
        if(!is_string($ns) || empty($ns))
        {
            $ns = 'default';
        }
        elseif(!isset($this->_title[$ns]))
        {
            $this->_title[$ns] = array();
        }
        array_unshift($this->_title[$ns], $title);
        return $this;
    }

    /**
     * Assemble title
     * @param string $ns
     * @param bool $reset
     * @return string
     */
    function getTitle($ns = 'default', $reset = false)
    {
        if(!is_string($ns) || empty($ns))
        {
            $ret = implode($this->_title_separator, $this->_title['default']);
            if($reset)
                $this->_title['default'] = '';
        }
        elseif(isset($this->_title[$ns]))
        {
            $ret = implode($this->_title_separator, $this->_title[$ns]);
            if($reset)
                unset($this->_title[$ns]);
        }
        else
        {
            $ret = '';
        }
        return $ret;
    }
    
    /**
     *
     * @param string $render_mod
     * @param mixed $ns
     * @return eResponse
     */
    function setRenderMod($render_mod, $ns = 'default')
    {
        $this->_render_mod[$ns] = $render_mod;
        return $this;
    }

    /**
     * Retrieve render mod
     * @param mixed $ns
     * @return mixed
     */
    function getRenderMod($ns = 'default')
    {
        if(!is_string($ns) || empty($ns))
        {
            $ns = 'default';
        }
        return vartrue($this->_render_mod[$ns], null);
    }
    
    /**
     * Generic meta information 
     * Example usage: 
     * addMeta('og:title', 'My Title');
     * addMeta(null, 30, array('http-equiv' => 'refresh'));
     * addMeta(null, null, array('http-equiv' => 'refresh', 'content' => 30)); // same as above
     * @param string $name 'name' attribute value, or null to avoid it
     * @param string $content 'content' attribute value, or null to avoid it
     * @param array $extended format 'attribute_name' => 'value'
     * @return eResponse
     */
    public function addMeta($name = null, $content = null, $extended = array())
    {
        if(empty($content)){ return $this; } // content is required, otherwise ignore. 
        
        //TODO need an option that allows subsequent entries to overwrite existing ones. 
        //ie. 'description' and 'keywords' should never be duplicated, but overwritten by plugins and other non-pref-based meta data. 




        $attr = array();
                
        if(null !== $name)
        {
        //    $key = (substr($name,0,3) == 'og:') ? 'property' : 'name';
        //    $attr[$key] = $name;
            if(!in_array($name, $this->_meta_name_only))
            {
                $attr['property'] = $name;  // giving both should be valid and avoid issues with FB and others.
            }

            if(!in_array($name, $this->_meta_property_only))
            {
                $attr['name'] = $name;
            }
        }



        if(null !== $content) $attr['content'] = $content;
        if(!empty($extended)) 
        {
            if(!empty($attr))  $attr = array_merge($attr, $extended);
            else $attr = $extended;
        }


        if(!empty($attr))
        {
            if(!in_array($name, $this->_meta_multiple)) // prevent multiple keyword tags.
            {
                $this->_meta[$name] = $attr;
            }
            else // multiple allowed.
            {
                $this->_meta[] = $attr;
            }
        }

        return $this;
    }
    
    /**
     * Render meta tags, registered via addMeta() method
     * @return string
     */
    public function renderMeta()
    {
        $attrData = '';

        e107::getEvent()->trigger('system_meta_pre', $this->_meta);

        $pref = e107::getPref();

        if(!empty($pref['meta_keywords'][e_LANGUAGE])) // Always append (global) meta keywords to the end.
        {
            $tmp1 = (array) explode(",", $this->getMetaKeywords());
            $tmp2 = (array) explode(",", $pref['meta_keywords'][e_LANGUAGE]);

            $tmp3 = array_unique(array_merge($tmp1,$tmp2));

            $this->setMeta('keywords', implode(',',$tmp3));
        }



        e107::getDebug()->log($this->_meta);


        foreach ($this->_meta as $attr)
        {
            $attrData .= '<meta';
            foreach ($attr as $p => $v) 
            {
                $attrData .= ' '.preg_replace('/[^\w\-]/', '', $p).'="'.str_replace(array('"', '<'), '', $v).'"';
            }
            $attrData .= ' />'."\n";
        }

        return $attrData;
    }

    /**
     * Add meta title, description and keywords
     *
     * @param string $meta property name
     * @param string $content meta content
     * @return eResponse
     */
    function addMetaData($meta, $content)
    {
        $meta = '_' . $meta;
        if(isset($this->$meta) && !empty($content))
        {
            $content = str_replace('&amp;', '&', $content);

            if($meta !== '_e_PAGETITLE' && $meta !== '_e_PAGETITLE_OVERRIDE')
            {
                $content = htmlspecialchars((string) $content, ENT_QUOTES, 'UTF-8');
            }

            $this->{$meta}[] = $content;
        }
        return $this;
    }
    
    /**
     * Get meta title, description and keywords
     *
     * @param string $meta property name
     * @return string
     */
    function getMetaData($meta, $separator = '')
    {
        $meta = '_' . $meta;
        if(isset($this->$meta) && !empty($this->$meta))
        {
            return implode($separator, $this->$meta);
        }
        return '';
    }



    /**
     * Return an array of all meta data
     * @return array
     */
    function getMeta()
    {
        return $this->_meta;
    }


    /**
     * @param string $title
     * @return eResponse
     */
    public function addMetaTitle($title, $reset=false, $override=false)
    {
        if($reset)
        {
            if($override)
            {
                $this->_e_PAGETITLE_OVERRIDE = array();
            }
            else
            {
                $this->_e_PAGETITLE = array();
            }
        }

        $title = str_replace(['&#39;','&#039;'], "'", $title);

        if($override)
        {
            return $this->addMetaData('e_PAGETITLE_OVERRIDE', $title);
        }

        return $this->addMetaData('e_PAGETITLE', $title);
    }

    /**
     * @return string
     */
    public function getMetaTitle($override = false)
    {
        if($override)
        {
            return $this->getMetaData('e_PAGETITLE_OVERRIDE', $this->_meta_title_separator);
        }

        return $this->getMetaData('e_PAGETITLE', $this->_meta_title_separator);
    }

    /**
     * @param string $description
     * @return eResponse
     */
    function addMetaDescription($description)
    {
        return $this->addMetaData('META_DESCRIPTION', $description);
    }

    /**
     * @return string
     */
    function getMetaDescription()
    {
        return $this->getMetaData('META_DESCRIPTION');
    }

    /**
     * @param string $keywords
     * @return eResponse
     */
    function addMetaKeywords($keywords)
    {
        return $this->addMetaData('META_KEYWORDS', $keywords);
    }

    /**
     * @return string
     */
    function getMetaKeywords()
    {
        return $this->getMetaData('META_KEYWORDS', ',');
    }

    /**
     * Send e107 meta-data
     * @return eResponse
     */
    function sendMeta()
    {
        //HEADERF already included or meta content already sent
        if(e_AJAX_REQUEST || defined('USER_AREA') || defined('e_PAGETITLE'))
            return $this;

        if(!defined('e_PAGETITLE') && !empty($this->_e_PAGETITLE))
        {
            define('e_PAGETITLE', $this->getMetaTitle());
        }
        if(!defined('META_DESCRIPTION') && !empty($this->_META_DESCRIPTION))
        {
            define('META_DESCRIPTION', $this->getMetaDescription());
        }
        if(!defined('META_KEYWORDS') && !empty($this->_META_KEYWORDS))
        {
            define('META_KEYWORDS', $this->getMetaKeywords());
        }
        return $this;
    }

    /**
     * Send Response Output - default method
     * TODO - ajax send, using js_manager
     * @param string $ns namespace/segment
     * @param bool $return
     * @param bool $render_message append system messages
     * @return null|string
     */
    function send($ns = null, $return = true, $render_message = true)
    {
        $content = $this->getBody($ns, true);
        $render = $this->getParam('render');
        $meta = $this->getParam('meta');
        
        $this->sendContentType();

        if($render_message)
        {
            $content = eMessage::getInstance()->render().$content;
        }

        if($meta)
        {
            $this->sendMeta();
        }

        //render disabled by the controller
        if(!$this->getRenderMod($ns))
        {
            $render = false;
        }

        if($render)
        {
            $render = e107::getRender();
            if($return)
            {
                return $render->tablerender($this->getTitle($ns, true), $content, $this->getRenderMod($ns), true);
            }
            else
            {
                $render->tablerender($this->getTitle($ns, true), $content, $this->getRenderMod($ns));
                return '';
            }
        }
        elseif($return)
        {
            return $content;
        }
        else
        {
            print $content;
            return '';
        }
    }
    
    /**
     * Send AJAX Json Response Output - default method
     * It's fully compatible with the core dialog.js
     * @param array $override override output associative array (header, body and footer keys)
     * @param string $ns namespace/segment
     * @param bool $render_message append system messages
     */
    function sendJson($override = array(), $ns = null, $render_message = true)
    {
        if(!$ns) $ns = 'default';
        
        $content = $this->getBody($ns, true);
        // separate render parameter for json response, false by default
        $render = $this->getParam('jsonRender');
        if($render_message)
        {
            $content = eMessage::getInstance()->render().$content;
        }

        //render disabled by the controller
        if(!$this->getRenderMod($ns))
        {
            $render = false;
        }
        
        
        $title = '';
        if(!$this->getParam('jsonNoTitle'))
        {
            $titleArray = $this->_title;
            $title = isset($titleArray[$ns]) ? array_pop($titleArray[$ns]) : '';
        } 

        if($render)
        {
            $render = e107::getRender();
            $content = $render->tablerender($this->getTitle($ns, true), $content, $this->getRenderMod($ns), true);
        }

        $jshelper = e107::getJshelper();
        $override = array_merge(array(
            'header' => $title,
            'body' => $content,
        //    'footer' => $statusText, // FIXME $statusText has no value.
        ), $override);
        echo $jshelper->buildJsonResponse($override);
        $jshelper->sendJsonResponse(null);
    }
    
    /**
     * JS manager 
     * @return e_jsmanager
     */
    function getJs()
    {
        return e107::getJs();
    }
}

/**
 * We move all generic helper functionallity here - a lot of candidates in e107 class
 * 
 */
class eHelper
{
    protected static $_classRegEx = '#[^\w\s\-]#';
    protected static $_idRegEx = '#[^\w\-]#';
    protected static $_styleRegEx = '#[^\w\s\-\.;:!]#';

    protected static $_systemNotify = 'systemNotifications';

    /**
     * @param $string
     * @return array|string|string[]|null
     */
    public static function secureClassAttr($string)
    {
        return preg_replace(self::$_classRegEx, '', $string);
    }


    /**
     * @return array
     */
    public static function getSystemNotification()
    {
        return (array) e107::getSession()->get(self::$_systemNotify);
    }


    /**
     * @param string $id
     * @param string $message
     * @return array|e_core_session|null
     */
    public static function addSystemNotification($id, $message)
    {
        return e107::getSession()->set(self::$_systemNotify.'/'.$id.'/message', $message);
    }


    /**
     * @param $id
     * @return array|e_core_session|null
     */
    public static function clearSystemNotification($id = '')
    {
        $key = !empty($id) ? self::$_systemNotify.'/'.$id : self::$_systemNotify;

        return e107::getSession()->clear($key);
    }


    /**
     * @param $string
     * @return array|string|string[]|null
     */
    public static function secureIdAttr($string)
    {
        $string = str_replace(array('/','_'),'-',$string);
        return preg_replace(self::$_idRegEx, '', $string);
    }

    /**
     * @param $string
     * @return array|string|string[]|null
     */
    public static function secureStyleAttr($string)
    {
        return preg_replace(self::$_styleRegEx, '', $string);
    }

    /**
     * @param $safeArray
     * @return string
     */
    public static function buildAttr($safeArray)
    {
        return http_build_query($safeArray, null, '&');
    }

    /**
     * @param $title
     * @return string
     */
    public static function formatMetaTitle($title)
    {
        $title = trim(str_replace(array('"', "'"), '', strip_tags(e107::getParser()->toHTML($title, TRUE))));
        return trim(preg_replace('/[\s,]+/', ' ', str_replace('_', ' ', $title)));
    }

    /**
     * @param $sef
     * @return string
     */
    public static function secureSef($sef)
    {
        return trim(preg_replace('/[^\w\pL\s\-+.,]+/u', '', strip_tags(e107::getParser()->toHTML($sef, TRUE))));
    }

    /**
     * @param $keywordString
     * @return string
     */
    public static function formatMetaKeys($keywordString)
    {
        $keywordString = preg_replace('/[^\w\pL\s\-.,+]/u', '', strip_tags(e107::getParser()->toHTML($keywordString, TRUE)));
        return trim(preg_replace('/[\s]?,[\s]?/', ',', str_replace('_', ' ', $keywordString)));
    }

    /**
     * @param $descrString
     * @return string
     */
    public static function formatMetaDescription($descrString)
    {
        $descrString = preg_replace('/[\r]*\n[\r]*/', ' ', trim(str_replace(array('"', "'"), '', strip_tags(e107::getParser()->toHTML($descrString, TRUE)))));
        return trim(preg_replace('/[\s]+/', ' ', str_replace('_', ' ', $descrString)));
    }

    /**
     * Convert title to valid SEF URL string
     * Type ending with 'l' stands for 'to lowercase', ending with 'c' - 'to camel case'
     * @param string $title
     * @param string $type dashl|dashc|dash|underscorel|underscorec|underscore|plusl|plusc|plus|none
     * @return array|string|string[]
     */
    public static function title2sef($title, $type = null)
    {
        /*$char_map = array(
            // Latin
            'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C',
            'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
            'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O',
            'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
            'ß' => 'ss',
            'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c',
            'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
            'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o',
            'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th',
            'ÿ' => 'y',
            // Latin symbols
            '©' => '(c)',
            // Greek
            'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8',
            'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P',
            'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W',
            'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I',
            'Ϋ' => 'Y',
            'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8',
            'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p',
            'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w',
            'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's',
            'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i',
            // Turkish
            'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G',
            'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g',
            // Russian
            'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh',
            'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O',
            'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
            'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu',
            'Я' => 'Ya',
            'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh',
            'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o',
            'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c',
            'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu',
            'я' => 'ya',
            // Ukrainian
            'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G',
            'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g',
            // Czech
            'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U',
            'Ž' => 'Z',
            'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u',
            'ž' => 'z',
            // Polish
            'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z',
            'Ż' => 'Z',
            'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z',
            'ż' => 'z',
            // Latvian
            'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N',
            'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z',
            'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n',
            'š' => 's', 'ū' => 'u', 'ž' => 'z'
        );*/

        $tp = e107::getParser();

        // issue #3245: strip all html and bbcode before processing
        $title = $tp->toText($title);

        $title = $tp->toASCII($title);

        $title = str_replace(array('/',' ',","),' ',$title);
        $title = str_replace(array("&","(",")"),'',$title);
        $title = preg_replace('/[^\w\pL\s.-]/u', '', strip_tags(e107::getParser()->toHTML($title, TRUE)));
        $title = trim(preg_replace('/[\s]+/', ' ', str_replace('_', ' ', $title)));
        $title = str_replace(array(' - ',' -','- ','--'),'-',$title); // cleanup to avoid ---

        $words = str_word_count($title,1, '1234567890');

        $limited = array_slice($words, 0, 14); // Limit number of words to 14. - any more and it ain't friendly.

        $title = implode(" ",$limited);

        if(null === $type)
        {
            $type = e107::getPref('url_sef_translate'); 
        }

        switch ($type) 
        {
            case 'dashl': //dasherize, to lower case
                return self::dasherize($tp->ustrtolower($title));
            break;
            
            case 'dashc': //dasherize, camel case
                return self::dasherize(self::camelize($title, true, ' '));
            break;
            
            case 'dash': //dasherize
                return self::dasherize($title);
            break;
            
            case 'underscorel': ///underscore, to lower case
                return self::underscore($tp->ustrtolower($title));
            break;
            
            case 'underscorec': ///underscore, camel case
                return self::underscore(self::camelize($title, true, ' '));
            break;
            
            case 'underscore': ///underscore
                return self::underscore($title);
            break;
            
            case 'plusl': ///plus separator, to lower case
                return str_replace(' ', '+', $tp->ustrtolower($title));
            break;
            
            case 'plusc': ///plus separator, to lower case
                return str_replace(' ', '+', self::camelize($title, true, ' '));
            break;
            
            case 'plus': ///plus separator
                return str_replace(' ', '+', $title);
            break;
            
            case 'none':
            default:
                return $title;
            break;
        }
    }
    
    /**
     * Return a memory value formatted helpfully
     * $dp overrides the number of decimal places displayed - realistically, only 0..3 are sensible
     * FIXME e107->parseMemorySize() START
     * - move here all e107 class ban/ip related methods
     * - out of (integer) range case?
     * 32 bit systems range: -2147483648 to 2147483647
     * 64 bit systems range: -9223372036854775808 9223372036854775807
     * {@link http://www.php.net/intval}
     * FIXME e107->parseMemorySize() END
     *
     * @param integer $size
     * @param integer $dp
     * @return string formatted size
     */
    public static function parseMemorySize($size, $dp = 2)
    {
        if (!$size) { $size = 0; }
        if ($size < 4096)
        {    // Fairly arbitrary limit below which we always return number of bytes
            return number_format($size, 0).CORE_LAN_B;
        }

        $size = $size / 1024;
        $memunit = CORE_LAN_KB;

        if ($size > 1024)
        { /* 1.002 mb, etc */
            $size = $size / 1024;
            $memunit = CORE_LAN_MB;
        }
        if ($size > 1024)
        { /* show in GB if >1GB */
            $size = $size / 1024;
            $memunit = CORE_LAN_GB;
        }
        if ($size > 1024)
        { /* show in TB if >1TB */
            $size = $size / 1024;
            $memunit = CORE_LAN_TB;
        }
        return (number_format($size, $dp).$memunit);
    }
    
    /**
     * Get the current memory usage of the code
     * If $separator argument is null, raw data (array) will be returned
     *
     * @param null|string $separator
     * @return string|array memory usage
     */
    public static function getMemoryUsage($separator = '/')
    {
        $ret = array();
        if(function_exists("memory_get_usage"))
        {
          $ret[] = eHelper::parseMemorySize(memory_get_usage());
          // With PHP>=5.2.0, can show peak usage as well
          if (function_exists("memory_get_peak_usage")) $ret[] = eHelper::parseMemorySize(memory_get_peak_usage(TRUE));
        }
        else
        {
          $ret[] = 'Unknown';
        }

        return (null !== $separator ? implode($separator, $ret) : $ret);
    }

    /**
     * @param $str
     * @param $all
     * @param $space
     * @return string
     */
    public static function camelize($str, $all = false, $space = '')
    {
        // clever recursion o.O
        if($all) return self::camelize('-'.$str, false, $space);
        
        $tmp = explode('-', str_replace(array('_', ' '), '-', e107::getParser()->ustrtolower($str)));
        return trim(implode($space, array_map('ucfirst', $tmp)), $space);
    }

    /**
     * @param $str
     * @param $space
     * @return string
     */
    public static function labelize($str, $space = ' ')
    {
        return self::camelize($str, true, ' ');
    }

    /**
     * @param $str
     * @return array|string|string[]
     */
    public static function dasherize($str)
    {
        return str_replace(array('_', ' '), '-', $str);
    }

    /**
     * @param $str
     * @return array|string|string[]
     */
    public static function underscore($str)
    {
        return str_replace(array('-', ' '), '_', $str);
    }

    /**
     * Parse generic shortcode parameter string
     * Format expected: {SC=key=val&key1=val1...}
     * Escape strings: \& => &
     *
     * @param string $parm
     * @return array associative param array
     */
    public static function scParams($parm)
    {
        if (!$parm) return array();
        if (!is_array($parm))
        {
            $parm = str_replace('\&', '%%__amp__%%', $parm);
            $parm = str_replace('&amp;', '&', $parm); // clean when it comes from the DB
            parse_str($parm, $parm);
            foreach ($parm as $k => $v)
            {
                $parm[str_replace('%%__amp__%%', '&', $k)] = str_replace('%%__amp__%%', '\&', $v);
            }
        }

        return $parm;
    }
    
    /**
     * Parse shortcode parameter string of type 'dual parameters' - advanced, more complex and slower(!) case
     * Format expected: {SC=name|key=val&key1=val1...}
     * Escape strings: \| => | , \& => & and \&amp; => &amp;
     * Return array is formatted like this: 
     * 1 => string|array (depends on $name2array value) containing first set of parameters; 
     * 2 => array containing second set of parameters;
     * 3 => string containing second set of parameters;
     * 
     * @param string $parmstr
     * @param boolean $first2array If true, first key (1) of the returned array will be parsed to array as well
     * @return array 
     */
    public static function scDualParams($parmstr, $first2array = false)
    {
        if (!$parmstr) return array(1 => '', 2 => array(), 3 => '');
        if (is_array($parmstr)) return $parmstr;

        $parmstr = str_replace('&amp;', '&', $parmstr); // clean when it comes from the DB
        $parm = explode('|', str_replace(array('\|', '\&amp;', '\&'), array('%%__pipe__%%', '%%__ampamp__%%', '%%__amp__%%'), $parmstr), 2);

        $multi = str_replace('%%__pipe__%%', '|', $parm[0]);
        if ($first2array)
        {
            parse_str($multi, $multi);
            foreach ($multi as $k => $v)
            {
                $multi[str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $k)] = str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $v);
            }
        }

        if (varset($parm[1]))
        {
            // second paramater as a string - allow to be further passed to shortcodes
            $parmstr = str_replace(array('%%__pipe__%%', '%%__ampamp__%%', '%%__amp__%%'), array('\|', '\&amp;', '\&'), $parm[1]);
            parse_str(str_replace('%%__pipe__%%', '|', $parm[1]), $params);
            foreach ($params as $k => $v)
            {
                $params[str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $k)] = str_replace(array('%%__ampamp__%%', '%%__amp__%%'), array('&amp;', '&'), $v);
            }
        }
        else
        {
            $parmstr = '';
            $params = array();
        }

        return array(1 => $multi, 2 => $params, 3 => $parmstr);
    }


    /**
     * Remove Social Media Trackers from a $_GET array based on key matches.
     * @param array $get
     * @return array
     */
    public static function removeTrackers($get = array())
    {
        $trackers = array('fbclid','utm_source','utm_medium','utm_content','utm_campaign','elan', 'msclkid', 'gclid', 'gad', 'gad_source', 'mcp_token');

        foreach($trackers as $val)
        {
            if(isset($get[$val]))
            {
                unset($get[$val]);
            }
        }

        return $get;

    }


    /**
     * Duplicates the value from a title form field into the meta-title form field.
     * @param str $titleID  eg. news-title
     * @param str $metaTitleID eg. news-meta-title
     * @return void
     */
    public static function syncSEOTitle($titleID, $metaTitleID)
    {

        e107::js('footer-inline', '

            $(window).on("load", function () {
            
                if(!$("#'.$metaTitleID.'").val()) 
                {
                    var title = $("#'.$titleID.'").val() + " | " + "'.SITENAME.'";
                    var charlimit = $("#'.$metaTitleID.'").attr("data-char-count");
                    
                    $("#'.$metaTitleID.'").attr("placeholder",title);

                    if(title.length > charlimit)
                    {
                       $("#'.$metaTitleID.'").addClass("has-error");
                    }
                }
                
            });


            $("#'.$metaTitleID.'").on("ready focus", function() {
                
                var title = $("#'.$titleID.'").val() + " | " + "'.SITENAME.'";
              
                if(!$(this).val()) 
                {
                    $(this).val(title);
                }
                else
                {
                    $(this).attr("placeholder",title);
                }
              
            });
            
        
            $("#'.$titleID.'").on("input change focus", function() 
            {
                var title = $("#'.$titleID.'").val() + " | " + "'.SITENAME.'";
                $("#'.$metaTitleID.'").attr("placeholder",title);
            
            });
            
        ');


    }


}