<?php
/**
 * router类从baseRouter类集成而来，您可以通过修改这个文件实现对baseRouter类的扩展。
 * The router class extend from baseRouter class, you can extend baseRouter class by change this file.
 *
 * @package framework
 *
 * The author disclaims copyright to this source code. In place of 
 * a legal notice, here is a blessing:
 *
 *  May you do good and not evil.
 *  May you find forgiveness for yourself and forgive others.
 *  May you share freely, never taking more than you give.
 */
define('FRAME_ROOT', dirname(__FILE__));
include FRAME_ROOT . '/base/router.class.php';
class router extends baseRouter
{
    /**
     * 标记是否是企业版
     * Whether the tag is a biz version
     *
     * @var bool
     * @access public
     */
    public $isBiz = false;

    /**
     * 标记是否是工作流
     * Whether the tag is a workflow
     *
     * @var bool
     * @access public
     */
    public $isFlow = false;

    /**
     * 请求的原始模块名。
     * The requestd module name parsed from a URL.
     *
     * @var string
     * @access public
     */
    public $rawModule;

    /**
     * 请求的原始方法名。
     * The requested method name parsed from a URL.
     *
     * @var string
     * @access public
     */
    public $rawMethod;

    /**
     * The root directory of static.
     * 
     * @var string
     * @access public 
     */
    public $staticRoot;

    /**
     * 构造方法, 设置路径，类，超级变量等。注意：
     * 1.应该使用createApp()方法实例化router类；
     * 2.如果$appRoot为空，框架会根据$appName计算应用路径。
     *
     * The construct function.
     * Prepare all the paths, classes, super objects and so on.
     * Notice: 
     * 1. You should use the createApp() method to get an instance of the router.
     * 2. If the $appRoot is empty, the framework will compute the appRoot according the $appName
     *
     * @param string $appName   the name of the app 
     * @param string $appRoot   the root path of the app
     * @access public
     * @return void
     */
    public function __construct($appName = 'demo', $appRoot = '')
    {
        parent::__construct($appName, $appRoot);
        $this->setAppName($appName);
        $this->setStaticRoot();
        $this->setDataRoot();
        $this->setThemeRoot();
        $this->setBiz();
    }

    /**
     * 创建一个应用。
     * Create an application.
     * 
     * @param string $appName   应用名称。  The name of the app.
     * @param string $appRoot   应用根路径。The root path of the app.
     * @param string $className 应用类名，如果对router类做了扩展，需要指定类名。When extends router class, you should pass in the child router class name.
     * @static
     * @access public
     * @return object   the app object
     */
    public static function createApp($appName = 'demo', $appRoot = '', $className = '')
    {
        if(empty($className)) $className = __CLASS__;
        return new $className($appName, $appRoot);
    }

    /**
     * Set the static root.
     * 
     * @access protected
     * @return void
     */
    public function setStaticRoot()
    {
        $this->staticRoot = $this->basePath . 'www' . DS;
    }

    /**
     * 设置www的根目录。
     * Set the www root.
     * 
     * @access public
     * @return void
     */
    public function setWwwRoot()
    {
        $this->wwwRoot = rtrim(dirname(dirname($_SERVER['SCRIPT_FILENAME'])), DS) . DS;
    }

    /**
     * Set the data root.
     * 
     * @access protected
     * @return void
     */
    public function setDataRoot()
    {
        $this->dataRoot = $this->staticRoot . 'data' . DS;
    }

    /**
     * Set the theme root.
     * 
     * @access protected
     * @return void
     */
    public function setThemeRoot()
    {
        $this->themeRoot = $this->staticRoot . 'theme' . DS;
    }

    /**
     * 设置模块的根目录。
     * Set the module root.
     * 
     * @access public
     * @return void
     */
    public function setModuleRoot()
    {
        $this->moduleRoot = $this->appRoot;
    }

   /**
     * 设置客户端的设备类型。
     * Set client device.
     * 
     * @access public
     * @return void
     */
    public function setClientDevice()
    {
        $mobile = new mobile();
        $this->clientDevice = ($mobile->isMobile() and !$mobile->isTablet()) ? 'mobile' : 'desktop';

        return $this->clientDevice;
    }

    /**
     * 设置是否是企业版。
     * Set whether is biz version.
     *
     * @access public
     * @return void
     */
    public function setBiz()
    {
        $this->isBiz = strpos($this->config->version, 'biz') === 0;
    }

    /**
     * Get the $staticRoot var
     * 
     * @access public
     * @return string
     */
    public function getStaticRoot()
    {
        return $this->staticRoot;
    }

    /**
     * PATH_INFO方式解析，获取$URI和$viewType。
     * Parse PATH_INFO, get the $URI and $viewType.
     *
     * @access public
     * @return void
     */
    public function parsePathInfo()
    {
        parent::parsePathInfo();

        if($this->get->display == 'card') $this->viewType = 'xhtml';
    }

    /**
     * GET请求方式解析，获取$URI和$viewType。
     * Parse GET, get $URI and $viewType.
     *
     * @access public
     * @return void
     */
    public function parseGET()
    {
        parent::parseGET();

        if($this->get->display == 'card') $this->viewType = 'xhtml';
    }

    /**
     * 获取$URL。
     * Get the $URL.
     * 
     * @param  bool $full  true, the URI contains the webRoot, else only hte URI.
     * @access public
     * @return string
     */
    public function getURI($full = false)
    {
        if($full and $this->config->requestType == 'PATH_INFO')
        {
            if($this->URI) return $this->config->webRoot . $this->appName . '/' . $this->URI . '.' . $this->viewType;
            return $this->config->webRoot . $this->appName . '/';
        }
        return $this->URI;
    }

    /**
     * 如果在指定应用的目录下未发现控制器文件，则到sys应用下再查找一次。
     * If the controller file is not found in the directory of the specified application, look it up again under the sys application.
     * @param  bool   $exitIfNone 
     * @access public
     * @return void
     */
    public function setZdooControlFile($exitIfNone)
    {
        $this->controlFile = $this->moduleRoot . $this->moduleName . DS . 'control.php';
        if(!is_file($this->controlFile)) $this->controlFile = $this->basePath . 'app' . DS . 'sys' . DS . $this->moduleName . DS . 'control.php';
        if(!is_file($this->controlFile))
        {
            $this->triggerError("the control file $this->controlFile not found.", __FILE__, __LINE__, $exitIfNone);
            return false;
        }
        return true;
    }

    /**
     * 检查请求的模块和方法是否应该调用工作流引擎进行处理。
     * Check if the requested module and method should call the workflow engine for processing.
     *
     * 处理逻辑：
     * Processing logic:
     * 1、如果当前版本不是企业版，或者当前请求处于安装模式或升级模式，调用父类方法并返回。
     * 1. If the current version is not the enterprise version, or if the current request is in install mode or upgrade mode, call the parent class method and return.
     *
     * 2、如果当前请求的模块在TABLE_WORKFLOW表中不存在，调用父类方法并返回。
     * 2. If the currently requested module does not exist in the TABLE_WORKFLOW table, call the parent class method and return.
     *
     * 3、如果当前请求的模块在TABLE_WORKFLOW表中存在并且是内置模块，并且请求的方法名是browselabel，则修改请求的模块名为flow，修改请求的方法名为browse，重新设置URI参数，调用父类方法并返回。
     * 3. If the currently requested module exists in the TABLE_WORKFLOW table and is a built-in module, and the requested method name is
     * browselabel, rename the module of the request to flow and the method of the request to browse, and reset the URI, call the parent class method and return.
     *
     * 4、如果不满足3中的条件但当前请求的方法在TABLE_WORKFLOWACTION表中存在，且方法扩展类型为重写，则修改请求的模块名为flow，方法名根据5中的规则修改，重新设置URI参数，调用父类方法并返回。
     * 4. If the condition of 3 is not satisfied but the currently requested method exists in the TABLE_WORKFLOWACTION table, and the method
     * extension type is overwrite, rename the module of the request to flow, and rename the method of the request according to the rule in 5.
     * Then reset the URI, call the parent class method and return.
     *
     * 5、如果当前请求的方法名为browse、create、edit、view、delete、export中任意一个，则方法名不变，否则方法名改为operate。
     * 5. If the currently requested method is named any one of browse, create, edit, view, delete, or export, the method name is unchanged, otherwise the method name is changed to operate.
     *
     * @param   bool    $exitIfNone     没有找到该控制器文件的情况：如果该参数为true，则终止程序；如果为false，则打印错误日志
     *                                  The controller file was not found: if the parameter is true, the program is terminated;
     *                                                                     if false, the error log is printed. 
     * @access  public
     * @return  bool
     */
    public function setControlFile($exitIfNone = true)
    {
        /* Set raw module and method name for fetch control. */
        if(empty($this->rawModule)) $this->rawModule = $this->moduleName;
        if(empty($this->rawMethod)) $this->rawMethod = $this->methodName;

        /* If is not a biz version or is in install mode or in in upgrade mode, call parent method. */
        if(!$this->isBiz or (defined('RUN_MODE') && (RUN_MODE == 'install' or RUN_MODE == 'upgrade'))) return $this->setZdooControlFile($exitIfNone);

        /* Check if the requested module is defined in workflow. */
        $flow = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOW . " WHERE `module` = '$this->moduleName'")->fetch();
        if(!$flow) return $this->setZdooControlFile($exitIfNone);

        /**
         * 工作流中配置的标签应该请求browse方法，而某些内置流程本身包含browse方法。在这里处理请求的时候会无法区分是内置的browse方法还是工作
         * 流标签的browse方法，为了避免此类冲突，在工作流中配置出的标签请求的方法改为browseLabel，在设置控制器文件时需要将其重设为browse。
         * Tags configured in the workflow should request the browse method, and some built-in processes themselves contain the browse
         * method. When processing a request here, it is impossible to distinguish between the built-in browse method and the browse
         * method of the workflow tag. In order to avoid such conflicts, the method of configuring the label request in the workflow
         * is changed to browseLabel, which needs to be reset to browse when setting the controller file.
         */
        if($flow->buildin && $this->methodName == 'browselabel')
        {
            $this->rawModule = $this->moduleName;
            $this->rawMethod = 'browse';
            $this->isFlow    = true;

            $moduleName = 'flow';
            $methodName = 'browse';

            $this->setFlowURI($moduleName, $methodName);
        }
        else
        {
            $action = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOWACTION . " WHERE `module` = '$this->moduleName' AND `action` = '$this->methodName'")->fetch();
            if(zget($action, 'extensionType') == 'override')
            {
                $this->rawModule = $this->moduleName;
                $this->rawMethod = $this->methodName;
                $this->isFlow    = true;

                $this->loadModuleConfig('workflowaction', 'flow');

                $moduleName = 'flow';
                $methodName = $this->methodName;
                /*
                 * 工作流中除了内置方法外的方法，如果是批量操作调用batchOperate方法，其它操作调用operate方法来执行。
                 * In addition to the built-in methods in the workflow, if the batch operation calls the batchOperate method, other operations call the operate method to execute.
                 */
                if(!in_array($this->methodName, $this->config->workflowaction->default->actions))
                {
                    if($action->type == 'single') $methodName = 'operate';
                    if($action->type == 'batch')  $methodName = 'batchOperate';
                }

                $this->setFlowURI($moduleName, $methodName);
            }
        }

        return $this->setZdooControlFile($exitIfNone);
    }

    /**
     * 把请求的URI重设成工作流引擎可以解析的URI。
     * Reset the requested URI to a URI that the workflow engine can resolve.
     *
     * e.g. /$module-browse-search-1.html   =>  /flow-browse-$module-search-1.html
     *      /$module-create.html            =>  /flow-create-$module.html
     *      /$module-edit-1.html            =>  /flow-edit-$module-1.html
     *      /$module-view-1.html            =>  /flow-view-$module-1.html
     *      /$module-delete-1.html          =>  /flow-delete-$module-1.html
     *      /$module-close-1.html           =>  /flow-operate-$module-close-1.html
     *
     *      /index.php?m=$module&f=browse&mode=search&label=1   =>  /index.php?m=flow&f=browse&module=$module&mode=search&label=1
     *      /index.php?m=$module&f=create&id=1                  =>  /index.php?m=flow&f=create&module=$module&$id=1
     *      /index.php?m=$module&f=edit&id=1                    =>  /index.php?m=flow&f=edit&module=$module&$id=1
     *      /index.php?m=$module&f=view&id=1                    =>  /index.php?m=flow&f=view&module=$module&$id=1
     *      /index.php?m=$module&f=delete&id=1                  =>  /index.php?m=flow&f=delete&module=$module&$id=1
     *      /index.php?m=$module&f=close&id=1                   =>  /index.php?m=flow&f=operate&module=$module&action=close&$id=1
     *
     * @param  string $moduleName
     * @param  string $methodName
     * @access public
     * @return void
     */
    public function setFlowURI($moduleName, $methodName)
    {
        $this->setModuleName($moduleName);
        $this->setMethodName($methodName);

        if($this->config->requestType != 'GET')
        {
            /* e.g. $this->URI = /$module-close-1.html. */
            $params = explode($this->config->requestFix, $this->URI);       // $params = array($module, 'close', 1);

            /* Remove module and method. */
            $params = array_slice($params, 2);                              // $params = array(1);

            /* Prepend other params. */
            if($methodName == 'operate' or $methodName == 'batchOperate')
            {
                array_unshift($params, $this->rawMethod);                   // $params = array('close', 1);
            }
            array_unshift($params, $this->rawModule);                       // $params = array($module, 'close', 1);
            array_unshift($params, $methodName);                            // $params = array('operate', $module, 'close', 1);
            array_unshift($params, $moduleName);                            // $params = array('flow', 'operate', $module, 'close', 1);

            $this->URI = implode($this->config->requestFix, $params);       // $this->URI = flow-operate-$module-close-1.html;
        }
        else
        {
            /* Extract $path and $query from $params. */
            /* e.g. $tshi->URI = /index.php?m=$module&f=close&id=1. */
            $params = parse_url($this->URI);                        // $params = array('path' => '/index.php', 'query' => m=$module&f=close&id=1;
            extract($params);                                       // $path = '/index.php'; $query = 'm=$module&f=close&id=1';
            parse_str($query, $params);                             // $params = array('m' => $module, 'f' => 'close', 'id' => 1);

            /* Remove module and method. */
            unset($params[$this->config->moduleVar]);               // $params = array('f' => 'close', 'id' => 1);
            unset($params[$this->config->methodVar]);               // $params = array('id' => 1);

            $params = array_reverse($params);                       // $params = array('id' => 1);

            /* Prepend other params. */
            if($methodName == 'operate' or $methodName == 'batchOperate')
            {
                $params['action'] = $this->rawMethod;               // $param = array('id' => 1, 'action' => 'close');;
            }
            $params['module']                 = $this->rawModule;   // $param = array('id' => 1, 'action' => 'close', 'module' => $module);
            $params[$this->config->methodVar] = $methodName;        // $param = array('id' => 1, 'action' => 'close', 'module' => $module, 'f' => 'operate');
            $params[$this->config->moduleVar] = $moduleName;        // $param = array('id' => 1, 'action' => 'close', 'module' => $module, 'f' => 'operate', 'm' => 'flow');

            $params = array_reverse($params);                       // $params = array('m' => 'flow', 'f' => 'operate', 'module' => $module, 'action' => 'close', 'id' => 1);

            $this->URI = $path . '?' . http_build_query($params);   // $this->URI = '/index.php?m=flow&f=operate&module=$module&action=close&id=1';
        }
    }

    /**
     * 获取一个模块的路径。
     * Get the path of one module.
     * 
     * @param  string $appName    the app name
     * @param  string $moduleName    the module name
     * @access public
     * @return string the module path
     */
    public function getModulePath($appName = '', $moduleName = '')
    {
        if($moduleName == '') $moduleName = $this->moduleName;
        $modulePath = parent::getModulePath($appName, $moduleName);
        if(!is_dir($modulePath)) $modulePath = parent::getModulePath('sys', $moduleName);

        return $modulePath;
    }

    /**
     * 获取一个模块的扩展路径。 Get extension path of one module.
     * 
     * If the extensionLevel == 0, return empty array.
     * If the extensionLevel == 1, return the common extension directory.
     * If the extensionLevel == 2, return the common and site extension directories.
     *
     * @param   string $appName        the app name
     * @param   string $moduleName     the module name
     * @param   string $ext            the extension type, can be control|model|view|lang|config
     * @access  public
     * @return  string the extension path.
     */
    public function getModuleExtPath($appName, $moduleName, $ext)
    {
        $paths    = parent::getModuleExtPath($appName, $moduleName, $ext);
        $sysPaths = parent::getModuleExtPath('sys', $moduleName, $ext);
        if(isset($paths['common']) and !is_dir($paths['common'])) $paths['common'] = $sysPaths['common'];
        if(isset($paths['site']) and !is_dir($paths['site']))     $paths['site']   = $sysPaths['site'];
        return $paths;
    }

    /**
     * 设置Action的扩展文件。 Set the action extension file.
     * 
     * @access  public
     * @return  bool
     */
    public function setActionExtFile()
    {
        $moduleExtPaths = $this->getModuleExtPath($this->appName, $this->moduleName, 'control');
        
        /* 如果扩展目录为空，不包含任何扩展文件。If there's no ext pathes return false.*/
        if(empty($moduleExtPaths)) $moduleExtPaths = $this->getModuleExtPath('sys', $this->moduleName, 'control');
        if(empty($moduleExtPaths)) return false;

        /* 如果extensionLevel == 2，且扩展文件存在，返回该站点扩展文件。If extensionLevel == 2 and site extensionFile exists, return it. */
        if($this->config->framework->extensionLevel == 2 and !empty( $moduleExtPaths['site']))
        {
            $this->extActionFile = $moduleExtPaths['site'] . $this->methodName . '.php';
            if(file_exists($this->extActionFile)) return true;
        }

        /* 然后再尝试寻找公共扩展文件。Then try to find the common extension file. */
        $this->extActionFile = $moduleExtPaths['common'] . $this->methodName . '.php';
        return file_exists($this->extActionFile);
    }

    /**
     * 设置一个模块的model文件，如果存在model扩展，一起合并。
     * Set the model file of one module. If there's an extension file, merge it with the main model file.
     * 
     * @param   string $moduleName the module name
     * @param   string $appName the app name
     * @static
     * @access  public
     * @return  string the model file
     */
    public function setModelFile($moduleName, $appName = '')
    {
        if($appName == '') $appName = $this->getAppName();

        /* 设置主model文件。 Set the main model file. */
        $mainModelFile = $this->getModulePath($appName, $moduleName) . 'model.php';
        if(!file_exists($mainModelFile)) $appName = 'sys';
        return parent::setModelFile($moduleName, $appName);
    }

    /**
     * 加载模块的config文件，返回全局$config对象。
     * 如果该模块是common，加载$configRoot的配置文件，其他模块则加载其模块的配置文件。
     *
     * Load config and return it as the global config object.
     * If the module is common, search in $configRoot, else in $modulePath.
     *
     * @param   string $moduleName     module name
     * @param   string $appName        app name
     * @param   bool   $exitIfNone     exit or not
     * @access  public
     * @return  object|bool the config object or false.
     */
    public function loadModuleConfig($moduleName, $appName = '')
    {
        global $config;

        if($config and (!isset($config->$moduleName) or !is_object($config->$moduleName))) $config->$moduleName = new stdclass();

        /* 初始化数组。Init the variables. */
        $extConfigFiles       = array();
        $commonExtConfigFiles = array();
        $siteExtConfigFiles   = array();

        /* 先获得模块的主配置文件。Get the main config file for current module first. */
        $mainConfigFile = $this->getModulePath($appName, $moduleName) . 'config.php';

        /* 查找扩展配置文件。Get extension config files. */
        if($config->framework->extensionLevel > 0) $extConfigPath = $this->getModuleExtPath($appName, $moduleName, 'config');
        if($config->framework->extensionLevel >= 1 and !empty($extConfigPath['common'])) $commonExtConfigFiles = helper::ls($extConfigPath['common'], '.php');
        if($config->framework->extensionLevel == 2 and !empty($extConfigPath['site']))   $siteExtConfigFiles   = helper::ls($extConfigPath['site'], '.php');
        $extConfigFiles = array_merge($commonExtConfigFiles, $siteExtConfigFiles);

        /* 将主配置文件和扩展配置文件合并在一起。Put the main config file and extension config files together. */
        $configFiles = array_merge(array($mainConfigFile), $extConfigFiles);

        /* 加载每一个配置文件。Load every config file. */
        static $loadedConfigs = array();
        foreach($configFiles as $configFile)
        {
            if(in_array($configFile, $loadedConfigs)) continue;
            if(file_exists($configFile)) include $configFile;
            $loadedConfigs[] = $configFile;
        }
        
        if($moduleName != 'common' and isset($config->system->$moduleName))
        {   
            helper::mergeConfig($config->system->$moduleName, $moduleName);
        }
        if($moduleName != 'common' and isset($config->personal->$moduleName))
        {   
            helper::mergeConfig($config->personal->$moduleName, $moduleName);
        }
    }

    /**
     * 加载语言文件，返回全局$lang对象。
     * Load lang and return it as the global lang object.
     * 
     * @param   string $moduleName     the module name
     * @param   string $appName     the app name
     * @access  public
     * @return  bool|object the lang object or false.
     */
    public function loadLang($moduleName, $appName = '')
    {
        if($moduleName == 'common' and $appName == '') $appName = 'sys';;
        $lang = parent::loadLang($moduleName, $appName);

        /* Merge from the db lang. */
        if(empty($appName)) $appName = $this->appName;

        $customLang = array();
        if(isset($lang->db->custom[$appName][$moduleName])) $customLang += $lang->db->custom[$appName][$moduleName];
        if(isset($lang->db->custom['sys'][$moduleName])) $customLang += $lang->db->custom['sys'][$moduleName];

        foreach($customLang as $section => $fields)
        {
            if(empty($section))
            {
                foreach($fields as $key => $value)
                {
                    if($moduleName == 'common')
                    {
                        unset($lang->{$key});
                        $lang->{$key} = $value;
                    }
                    else
                    {
                        if(!isset($lang->{$moduleName})) $lang->{$moduleName} = new stdclass();
                        unset($lang->{$moduleName}->{$key});
                        $lang->{$moduleName}->{$key} = $value;
                    }
                }
            }
            else
            {
                foreach($fields as $key => $value)
                {
                    if($moduleName == 'common')
                    {
                        unset($lang->{$section}[$key]);
                        $lang->{$section}[$key] = $value;
                    }
                    else
                    {
                        unset($lang->{$moduleName}->{$section}[$key]);
                        $lang->{$moduleName}->{$section}[$key] = $value;
                    }
                }
            }
        }

        $this->lang = $lang;
        return $lang;
    }

    /**
     * 如果$this->isFlow的值为true，说明这个请求需要工作流引擎来处理，则要根据工作流引擎的需要重新设置参数。
     * If the values of $this->isFlow is true, indicating that the request needs to be processed
     * by the workflow engine, the parameters are reset according to the needs of the workflow engine.
     *
     * @param   array $defaultParams     the default params defined by the method.
     * @param   array $passedParams      the params passed in through url.
     * @access  public
     * @return  array the merged params.
     */
    public function mergeParams($defaultParams, $passedParams)
    {
        /* If the isFlow is true, reset the passed params. */
        if($this->isFlow)
        {
            $passedParams = array_reverse($passedParams);

            /* 如果请求的方法名是operate或者batchOperate，则需要添加action参数来传递请求的方法名。 */
            /* If the requested method name is operate or batchOperate, need to add an action parameter to pass the requested method name. */
            $methodName = $this->getMethodName();
            if($methodName == 'operate' || $methodName == 'batchoperate') $passedParams['action'] = $this->rawMethod;

            /* 添加module参数来传递请求的模块名。 */
            /* Add the module parameter to pass the requested module name. */
            $passedParams['module'] = $this->rawModule;

            $passedParams = array_reverse($passedParams);
        }

        /* display参数用来标记请求是否来自喧喧客户端的卡片展示页面，此处应该删掉以避免对方法调用产生影响。 */
        /* The display parameter is used to mark whether the request comes from the card display page of the xuanxuan client. It should be deleted here to avoid affecting the method call. */
        unset($passedParams['display']);

        return parent::mergeParams($defaultParams, $passedParams);
    }
}
