diff --git a/.htaccess b/.htaccess index ae3580590..71d1f48ee 100644 --- a/.htaccess +++ b/.htaccess @@ -13,32 +13,7 @@ RewriteRule ^(.+)/(addons|files|layouts|m\.layouts|modules|widgets|widgetstyles) RewriteCond %{SCRIPT_FILENAME} !-f RewriteRule ^(.+)\.min\.(css|js)$ ./$1.$2 [L] -# rss, blogAPI -RewriteRule ^(rss|atom)$ ./index.php?module=rss&act=$1 [L] -RewriteRule ^([a-zA-Z0-9_]+)/(rss|atom|api)$ ./index.php?mid=$1&act=$2 [L] - -# trackback -RewriteRule ^([0-9]+)/(.+)/trackback$ ./index.php?document_srl=$1&key=$2&act=trackback [L] -RewriteRule ^([a-zA-Z0-9_]+)/([0-9]+)/(.+)/trackback$ ./index.php?mid=$1&document_srl=$2&key=$3&act=trackback [L] - -# document category -RewriteRule ^([a-zA-Z0-9_]+)/category/([0-9]+)$ ./index.php?mid=$1&category=$2 [L,QSA] - -# document permanent link -RewriteRule ^([0-9]+)$ ./index.php?document_srl=$1 [L,QSA] - -# admin module link -RewriteRule ^admin/?$ ./index.php?module=admin [L,QSA] - -# mid link +# all other short URLs +RewriteCond %{SCRIPT_FILENAME} !-f RewriteCond %{SCRIPT_FILENAME} !-d -RewriteRule ^([a-zA-Z0-9_]+)/?$ ./index.php?mid=$1 [L,QSA] - -# mid + document link -RewriteRule ^([a-zA-Z0-9_]+)/([0-9]+)$ ./index.php?mid=$1&document_srl=$2 [L,QSA] - -# mid + entry title -RewriteRule ^([a-zA-Z0-9_]+)/entry/(.+)$ ./index.php?mid=$1&entry=$2 [L,QSA] - -# file download -RewriteRule ^files/download/([0-9]+)/([a-zA-Z0-9_-]+)/(.+)$ ./index.php?act=procFileOutput&file_srl=$1&file_key=$2&filename=$3 [L] +RewriteRule . index.php [L] diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index 02930c18f..6e356dcf8 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -8,12 +8,6 @@ */ class Context { - /** - * Allow rewrite - * @var bool TRUE: using rewrite mod, FALSE: otherwise - */ - public $allow_rewrite = FALSE; - /** * Request method * @var string GET|POST|XMLRPC|JSON @@ -133,18 +127,16 @@ class Context */ private static $_init_called = false; + /** + * Current route information + */ + private static $_route_info = null; /** * object oFrontEndFileHandler() * @var object */ private static $_oFrontEndFileHandler = null; - /** - * SSL action cache file - * @var array - */ - private static $_ssl_actions_cache_file = 'files/cache/common/ssl_actions.php'; - /** * SSL action cache */ @@ -205,13 +197,6 @@ class Context self::$_oFrontEndFileHandler = self::$_instance->oFrontEndFileHandler = new FrontEndFileHandler(); self::$_get_vars = self::$_get_vars ?: new stdClass; self::$_tpl_vars = self::$_tpl_vars ?: new stdClass; - - // Include SSL action cache file. - self::$_ssl_actions_cache_file = RX_BASEDIR . self::$_ssl_actions_cache_file; - if(Rhymix\Framework\Storage::exists(self::$_ssl_actions_cache_file)) - { - self::$_ssl_actions = (include self::$_ssl_actions_cache_file) ?: array(); - } } return self::$_instance; } @@ -246,15 +231,26 @@ class Context self::$_instance = self::getInstance(); } + // Load system configuration. + self::loadDBInfo(); + // Set information about the current request. self::_checkGlobalVars(); self::setRequestMethod(); - self::setRequestArguments(); + if (in_array(self::$_instance->request_method, array('GET', 'POST'))) + { + $method = $_SERVER['REQUEST_METHOD'] ?: 'GET'; + $url = $_SERVER['REQUEST_URI']; + $route_info = Rhymix\Framework\Router::parseURL($method, $url, Rhymix\Framework\Router::getRewriteLevel()); + self::setRequestArguments($route_info->args); + self::$_route_info = $route_info; + } + else + { + self::setRequestArguments(); + } self::setUploadInfo(); - // Load system configuration. - self::loadDBInfo(); - // If Rhymix is installed, get virtual site information. if(self::isInstalled()) { @@ -266,8 +262,7 @@ class Context define('RX_BASEURL', parse_url($default_url, PHP_URL_PATH)); } } - $oModuleModel = ModuleModel::getInstance(); - $site_module_info = $oModuleModel->getDefaultMid() ?: new stdClass; + $site_module_info = ModuleModel::getDefaultMid() ?: new stdClass; self::set('site_module_info', $site_module_info); self::set('_default_timezone', ($site_module_info->settings && $site_module_info->settings->timezone) ? $site_module_info->settings->timezone : null); self::set('_default_url', self::$_instance->db_info->default_url = self::getDefaultUrl($site_module_info)); @@ -286,9 +281,19 @@ class Context } // Redirect to SSL if the current domain always uses SSL. - if ($site_module_info->security === 'always' && !RX_SSL && PHP_SAPI !== 'cli' && !$site_module_info->is_default_replaced) + if (!RX_SSL && PHP_SAPI !== 'cli' && $site_module_info->security === 'always' && !$site_module_info->is_default_replaced) { - $ssl_url = self::getDefaultUrl($site_module_info) . RX_REQUEST_URL; + $ssl_url = self::getDefaultUrl($site_module_info, true) . RX_REQUEST_URL; + self::setCacheControl(0); + header('Location: ' . $ssl_url, true, 301); + exit; + } + + // Redirect to SSL if the current action requires SSL. + self::$_ssl_actions = $site_module_info->security === 'optional' ? ModuleModel::getActionSecurity() : array(); + if (!RX_SSL && count(self::$_ssl_actions) && self::isExistsSSLAction(self::get('act')) && self::getRequestMethod() === 'GET') + { + $ssl_url = self::getDefaultUrl($site_module_info, true) . RX_REQUEST_URL; self::setCacheControl(0); header('Location: ' . $ssl_url, true, 301); exit; @@ -380,8 +385,6 @@ class Context // set authentication information in Context and session if (self::isInstalled()) { - $oModuleModel->loadModuleExtends(); - if (Rhymix\Framework\Session::getMemberSrl()) { MemberController::getInstance()->setSessionInfo(); @@ -507,7 +510,6 @@ class Context // Copy to old format for backward compatibility. self::$_instance->db_info = self::convertDBInfo($config); - self::$_instance->allow_rewrite = self::$_instance->db_info->use_rewrite === 'Y'; } /** @@ -616,12 +618,22 @@ class Context return self::$_instance->db_info; } + /** + * Get current route information + * + * @return object + */ + public static function getRouteInfo() + { + return self::$_route_info; + } + /** * Return ssl status * * @return object SSL status (Optional - none|always|optional) */ - public static function getSslStatus() + public static function getSSLStatus() { return self::get('_use_ssl'); } @@ -630,9 +642,10 @@ class Context * Return default URL * * @param object $site_module_info (optional) + * @param bool $use_ssl (optional) * @return string Default URL */ - public static function getDefaultUrl($site_module_info = null) + public static function getDefaultUrl($site_module_info = null, $use_ssl = null) { if ($site_module_info === null && ($default_url = self::get('_default_url'))) { @@ -644,9 +657,9 @@ class Context $site_module_info = self::get('site_module_info'); } - $prefix = $site_module_info->security === 'always' ? 'https://' : 'http://'; + $prefix = ($site_module_info->security === 'always' || $use_ssl) ? 'https://' : 'http://'; $hostname = $site_module_info->domain; - $port = $site_module_info->security === 'always' ? $site_module_info->https_port : $site_module_info->http_port; + $port = ($prefix === 'https://') ? $site_module_info->https_port : $site_module_info->http_port; $result = $prefix . $hostname . ($port ? sprintf(':%d', $port) : '') . RX_BASEURL; return $result; } @@ -1183,11 +1196,21 @@ class Context /** * handle request arguments for GET/POST * + * @param array $router_args * @return void */ - public static function setRequestArguments() + public static function setRequestArguments(array $router_args = []) { - foreach($_REQUEST as $key => $val) + $request_args = $_SERVER['REQUEST_METHOD'] === 'GET' ? $_GET : $_POST; + if (count($router_args)) + { + foreach ($router_args as $key => $val) + { + $request_args[$key] = $val; + } + } + + foreach($request_args as $key => $val) { if($val === '' || isset(self::$_reserved_keys[$key]) || self::get($key)) { @@ -1196,22 +1219,11 @@ class Context $key = escape($key); $val = self::_filterRequestVar($key, $val); - $set_to_vars = false; - - if($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET[$key])) - { - $set_to_vars = true; - } - elseif($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST[$key])) - { - $set_to_vars = true; - } - - self::set($key, $val, $set_to_vars); + self::set($key, $val, true); } // Set deprecated request parameters. - if(!$_POST && !empty($GLOBALS['HTTP_RAW_POST_DATA'])) + if($_SERVER['REQUEST_METHOD'] === 'POST' && !$_POST && !empty($GLOBALS['HTTP_RAW_POST_DATA'])) { if(self::getRequestMethod() === 'XMLRPC') { @@ -1231,6 +1243,10 @@ class Context foreach((array)$params as $key => $val) { + if (isset($request_args[$key])) + { + continue; + } $key = escape($key); $val = self::_filterXmlVars($key, $val); self::set($key, $val, true); @@ -1242,6 +1258,10 @@ class Context parse_str($GLOBALS['HTTP_RAW_POST_DATA'], $params); foreach($params as $key => $val) { + if (isset($request_args[$key])) + { + continue; + } $key = escape($key); $val = self::_filterRequestVar($key, $val); self::set($key, $val, true); @@ -1434,7 +1454,15 @@ class Context { $_val = (int)$_val; } - elseif(in_array($key, array('mid', 'vid', 'search_target', 'search_keyword', 'xe_validator_id')) || $_SERVER['REQUEST_METHOD'] === 'GET') + elseif(in_array($key, array('mid', 'vid', 'act', 'module'))) + { + $_val = preg_match('/^[a-zA-Z0-9_-]*$/', $_val) ? $_val : null; + if($_val === null) + { + self::$_instance->security_check = 'DENY ALL'; + } + } + elseif(in_array($key, array('search_target', 'search_keyword', 'xe_validator_id')) || $_SERVER['REQUEST_METHOD'] === 'GET') { $_val = escape($_val, false); if(ends_with('url', $key, false)) @@ -1613,6 +1641,12 @@ class Context { static $current_domain = null; static $site_module_info = null; + static $rewrite_level = null; + if ($rewrite_level === null) + { + $rewrite_level = Rhymix\Framework\Router::getRewriteLevel(); + } + if ($site_module_info === null) { $site_module_info = self::get('site_module_info'); @@ -1721,35 +1755,7 @@ class Context $query = ''; if(count($get_vars) > 0) { - // if using rewrite mod - if(self::$_instance->allow_rewrite) - { - $var_keys = array_keys($get_vars); - sort($var_keys); - $target = join('.', $var_keys); - $act = $get_vars['act']; - $mid = $get_vars['mid']; - $key = $get_vars['key']; - $srl = $get_vars['document_srl']; - $tmpArray = array('rss' => 1, 'atom' => 1, 'api' => 1); - $is_feed = isset($tmpArray[$act]); - $target_map = array( - 'mid' => $mid, - 'category.mid' => "$mid/category/" . $get_vars['category'], - 'entry.mid' => "$mid/entry/" . $get_vars['entry'], - 'document_srl' => $srl, - 'document_srl.mid' => "$mid/$srl", - 'act' => ($is_feed && $act !== 'api') ? $act : '', - 'act.mid' => $is_feed ? "$mid/$act" : '', - 'act.document_srl.key' => ($act == 'trackback') ? "$srl/$key/$act" : '', - 'act.document_srl.key.mid' => ($act == 'trackback') ? "$mid/$srl/$key/$act" : '', - ); - $query = $target_map[$target]; - } - if(!$query && count($get_vars) > 0) - { - $query = 'index.php?' . http_build_query($get_vars); - } + $query = Rhymix\Framework\Router::getURL($get_vars, $rewrite_level); } // If using SSL always @@ -2008,40 +2014,25 @@ class Context */ public static function addSSLAction($action) { - if(isset(self::$_ssl_actions[$action])) + if (!ModuleModel::getActionSecurity($action)) { - return; + getController('module')->insertActionSecurity($action); } - - self::$_ssl_actions[$action] = 1; - $buff = 'deleteActionSecurity($action); } - unset(self::$_ssl_actions[$action]); - $buff = 'allow_rewrite; + return Rhymix\Framework\Router::getRewriteLevel(); } /** diff --git a/classes/display/HTMLDisplayHandler.php b/classes/display/HTMLDisplayHandler.php index 8023f74fa..11d6848c5 100644 --- a/classes/display/HTMLDisplayHandler.php +++ b/classes/display/HTMLDisplayHandler.php @@ -204,17 +204,14 @@ class HTMLDisplayHandler // handles a relative path generated by using the rewrite module if(Context::isAllowRewrite()) { - $pattern = '/src=("|\'){1}(?:\.\/)?((?:files\/(?:attach|cache|faceOff|member_extra_info|thumbnails)|addons|common|(?:m\.)?layouts|modules|widgets|widgetstyle)\/[^"\']+)("|\'){1}/s'; + $pattern = '/(action|src|href)=(["\'])(?:\.\/([^"\']*))?(["\'])/s'; + $output = preg_replace($pattern, '$1=$2' . \RX_BASEURL . '$3$4', $output); + + $pattern = '/src=(["\'])((?:files\/(?:attach|cache|faceOff|member_extra_info|thumbnails)|addons|common|(?:m\.)?layouts|modules|widgets|widgetstyle)\/[^"\']+)(["\'])/s'; $output = preg_replace($pattern, 'src=$1' . \RX_BASEURL . '$2$3', $output); - $pattern = '/href=("|\'){1}(\?[^"\']+)/s'; + $pattern = '/href=(["\'])(\?[^"\']+)/s'; $output = preg_replace($pattern, 'href=$1' . \RX_BASEURL . '$2', $output); - - if(Context::get('vid')) - { - $pattern = '/\/' . Context::get('vid') . '\?([^=]+)=/is'; - $output = preg_replace($pattern, '/?$1=', $output); - } } // prevent the 2nd request due to url(none) of the background-image diff --git a/classes/module/ModuleHandler.class.php b/classes/module/ModuleHandler.class.php index d79e8122c..bce2365ae 100644 --- a/classes/module/ModuleHandler.class.php +++ b/classes/module/ModuleHandler.class.php @@ -12,15 +12,17 @@ * */ class ModuleHandler extends Handler { - - var $module = NULL; ///< Module - var $act = NULL; ///< action - var $mid = NULL; ///< Module ID - var $document_srl = NULL; ///< Document Number - var $module_srl = NULL; ///< Module Number - var $module_info = NULL; ///< Module Info - var $error = NULL; ///< an error code. - var $httpStatusCode = NULL; ///< http status code. + var $method = 'GET'; + var $module_info = null; + var $module_srl = null; + var $module = null; + var $act = null; + var $mid = null; + var $document_srl = null; + var $route = null; + var $error = null; + var $is_mobile = false; + var $httpStatusCode = 200; /** * Valid types and kinds of module instances. @@ -79,11 +81,14 @@ class ModuleHandler extends Handler } // Set variables from request arguments + $this->method = Context::getRequestMethod(); $this->module = $module ? $module : Context::get('module'); $this->act = $act ? $act : Context::get('act'); $this->mid = $mid ? $mid : Context::get('mid'); $this->document_srl = $document_srl ? (int) $document_srl : (int) Context::get('document_srl'); $this->module_srl = $module_srl ? (int) $module_srl : (int) Context::get('module_srl'); + $this->route = Context::getRouteInfo() ?: new stdClass; + $this->is_mobile = Mobile::isFromMobilePhone(); if($entry = Context::get('entry')) { $this->entry = Context::convertEncodingStr($entry); @@ -94,49 +99,13 @@ class ModuleHandler extends Handler Context::set('mid', $this->mid = null); } - // Validate variables to prevent XSS - $isInvalid = false; - if($this->module && !preg_match('/^[a-zA-Z0-9_-]+$/', $this->module)) - { - $isInvalid = true; - } - if($this->mid && !preg_match('/^[a-zA-Z0-9_-]+$/', $this->mid)) - { - $isInvalid = true; - } - if($this->act && !preg_match('/^[a-zA-Z0-9_-]+$/', $this->act)) - { - $isInvalid = true; - } - if($isInvalid) - { - $this->error = 'msg_security_violation'; - return; - } - - if(isset($this->act) && (strlen($this->act) >= 4 && substr_compare($this->act, 'disp', 0, 4) === 0)) - { - if(Context::get('_use_ssl') == 'optional' && Context::isExistsSSLAction($this->act) && !RX_SSL) - { - if(Context::get('_https_port') != null) - { - header('location: https://' . $_SERVER['HTTP_HOST'] . ':' . Context::get('_https_port') . $_SERVER['REQUEST_URI']); - } - else - { - header('location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); - } - return; - } - } - // call a trigger before moduleHandler init self::triggerCall('moduleHandler.init', 'before', $this); // execute addon (before module initialization) $called_position = 'before_module_init'; $oAddonController = AddonController::getInstance(); - $addon_file = $oAddonController->getCacheFilePath(Mobile::isFromMobilePhone() ? 'mobile' : 'pc'); + $addon_file = $oAddonController->getCacheFilePath($this->is_mobile ? 'mobile' : 'pc'); if(file_exists($addon_file)) include($addon_file); } @@ -146,13 +115,12 @@ class ModuleHandler extends Handler * */ public function init() { - $oModuleModel = ModuleModel::getInstance(); $site_module_info = Context::get('site_module_info'); // Check unregistered domain action. if (!$site_module_info || !isset($site_module_info->domain_srl) || $site_module_info->is_default_replaced) { - $site_module_info = $oModuleModel->getDefaultDomainInfo(); + $site_module_info = ModuleModel::getDefaultDomainInfo(); if ($site_module_info) { $domain_action = config('url.unregistered_domain_action') ?: 'redirect_301'; @@ -177,112 +145,65 @@ class ModuleHandler extends Handler } } - // if success_return_url and error_return_url is incorrect - $urls = array(Context::get('success_return_url'), Context::get('error_return_url')); - foreach($urls as $url) + // Check success_return_url and error_return_url to prevent dangerous redirects. + $urls = array('success_return_url', 'error_return_url'); + foreach($urls as $key) { - if(empty($url)) + $url = Context::get($key); + if ($url && !Rhymix\Framework\URL::isInternalURL($url)) { - continue; - } - - if($host = parse_url($url, PHP_URL_HOST)) - { - $defaultHost = parse_url(Context::getDefaultUrl(), PHP_URL_HOST); - if($host !== $defaultHost) - { - $siteModuleHost = $site_module_info->domain; - if(strpos($siteModuleHost, '/') !== false) - { - $siteModuleHost = parse_url($siteModuleHost, PHP_URL_HOST); - } - if($host !== $siteModuleHost) - { - Context::set('success_return_url', null); - Context::set('error_return_url', null); - } - } + Context::set($key, null); } } + // If the Router returned an error earlier, show an error here. + if($this->route && $this->route->status > 200) + { + $this->error = 'msg_module_is_not_exists'; + $this->httpStatusCode = 404; + return true; + } + // Convert document alias (entry) to document_srl if(!$this->document_srl && $this->mid && $this->entry) { - $oDocumentModel = DocumentModel::getInstance(); - $this->document_srl = $oDocumentModel->getDocumentSrlByAlias($this->mid, $this->entry); + $this->document_srl = DocumentModel::getDocumentSrlByAlias($this->mid, $this->entry); if($this->document_srl) { Context::set('document_srl', $this->document_srl); } } - // Get module's information based on document_srl, if it's specified + // Get module info from document_srl. if($this->document_srl) { - $module_info = $oModuleModel->getModuleInfoByDocumentSrl($this->document_srl); - if($module_info) + $module_info = $this->_checkDocumentSrl(); + if ($module_info === false) { - // If it exists, compare mid based on the module information - // if mids are not matching, set it as the document's mid - if(!$this->mid || ($this->mid != $module_info->mid)) - { - if(Context::getRequestMethod() == 'GET') - { - Context::setCacheControl(0); - header('location: ' . getNotEncodedSiteUrl($site_module_info->domain, 'mid', $module_info->mid, 'document_srl', $this->document_srl), true, 301); - return false; - } - else - { - $this->mid = $module_info->mid; - Context::set('mid', $this->mid); - } - } - // if requested module is different from one of the document, remove the module information retrieved based on the document number - if($this->module && $module_info->module != $this->module) - { - unset($module_info); - } - } - - // Block access to secret or temporary documents. - if(Context::getRequestMethod() == 'GET') - { - $oDocumentModel = DocumentModel::getInstance(); - $oDocument = $oDocumentModel->getDocument($this->document_srl); - if($oDocument->isExists() && !$oDocument->isAccessible()) - { - $this->httpStatusCode = '403'; - } + return false; } } - - // If module_info is not set yet, and there exists mid information, get module information based on the mid + + // Get module info from mid. if(!$module_info && $this->mid) { - $module_info = $oModuleModel->getModuleInfoByMid($this->mid, $site_module_info->site_srl); - //if($this->module && $module_info->module != $this->module) unset($module_info); + $module_info = ModuleModel::getModuleInfoByMid($this->mid); } - // If module_info is not set still, and $module does not exist, find the default module + // Set module info as the default module for the domain. if(!$module_info && !$this->module && !$this->mid) { $module_info = $site_module_info; } - if(!$module_info && !$this->module && $site_module_info->module_site_srl) - { - $module_info = $site_module_info; - } - - // Set index document - if($site_module_info->index_document_srl && !$this->module && !$this->mid && !$this->document_srl && Context::getRequestMethod() === 'GET' && !count($_GET)) + // Set the index document. + if($site_module_info->index_document_srl && !$this->module && !$this->mid && !$this->document_srl && $this->method === 'GET' && !$this->route->args) { Context::set('document_srl', $this->document_srl = $site_module_info->index_document_srl, true); } - // redirect, if site start module - if(!$site_module_info->index_document_srl && Context::getRequestMethod() === 'GET' && isset($_GET['mid']) && $_GET['mid'] === $site_module_info->mid && count($_GET) === 1) + // Redirect if the index module was requested. + if(!$site_module_info->index_document_srl && $this->method === 'GET' && isset($this->route->args['mid']) && $this->route->args['mid'] === $site_module_info->mid && count($this->route->args) === 1) { Context::setCacheControl(0); header('location: ' . getNotEncodedSiteUrl($site_module_info->domain), true, 301); @@ -292,57 +213,18 @@ class ModuleHandler extends Handler // If module info was set, retrieve variables from the module information if($module_info) { + // Set instance variables and SEO info. $this->module = $module_info->module; $this->mid = $module_info->mid; $this->module_info = $module_info; - if ($module_info->mid == $site_module_info->mid) - { - $seo_title = config('seo.main_title') ?: '$SITE_TITLE - $SITE_SUBTITLE'; - } - else - { - $seo_title = config('seo.subpage_title') ?: '$SITE_TITLE - $SUBPAGE_TITLE'; - } + $this->_setModuleSEOInfo($module_info, $site_module_info); - ModuleController::getInstance()->replaceDefinedLangCode($seo_title); - Context::setBrowserTitle($seo_title, array( - 'site_title' => Context::getSiteTitle(), - 'site_subtitle' => Context::getSiteSubtitle(), - 'subpage_title' => $module_info->browser_title, - 'page' => Context::get('page') ?: 1, - )); - - $module_config = $oModuleModel->getModuleConfig('module'); - if ($module_info->meta_keywords) - { - Context::addMetaTag('keywords', $module_info->meta_keywords); - } - elseif ($site_module_info->settings->meta_keywords) - { - Context::addMetaTag('keywords', $site_module_info->settings->meta_keywords); - } - elseif ($module_config->meta_keywords) - { - Context::addMetaTag('keywords', $module_config->meta_keywords); - } - - if ($module_info->meta_description) - { - Context::addMetaTag('description', $module_info->meta_description); - } - elseif ($site_module_info->settings->meta_description) - { - Context::addMetaTag('description', $site_module_info->settings->meta_description); - } - elseif($module_config->meta_description) - { - Context::addMetaTag('description', $module_config->meta_description); - } - - $viewType = (Mobile::isFromMobilePhone()) ? 'M' : 'P'; + // Check if the current request is from a mobile device. + $this->is_mobile = Mobile::isFromMobilePhone(); + $viewType = $this->is_mobile ? 'M' : 'P'; $targetSrl = $viewType === 'M' ? 'mlayout_srl' : 'layout_srl'; - // use the site default layout. + // Apply default layouts. if($module_info->{$targetSrl} == -1) { $oLayoutAdminModel = getAdminModel('layout'); @@ -363,29 +245,28 @@ class ModuleHandler extends Handler $layoutSrl = $module_info->{$targetSrl}; } - // reset a layout_srl in module_info. + // Reset layout_srl in module_info. $module_info->{$targetSrl} = $layoutSrl; - - $part_config = $oModuleModel->getModulePartConfig('layout', $layoutSrl); + + // Add layout header script. + $part_config = ModuleModel::getModulePartConfig('layout', $layoutSrl); Context::addHtmlHeader($part_config->header_script); } - - // Set module and mid into module_info - if(!isset($this->module_info)) + else { - $this->module_info = new stdClass(); + $this->module_info = new stdClass; + $this->module_info->module = $this->module; + $this->module_info->mid = $this->mid; } - $this->module_info->module = $this->module; - $this->module_info->mid = $this->mid; - // Set site_srl add 2011 08 09 + // Always overwrite site_srl (deprecated) $this->module_info->site_srl = $site_module_info->site_srl; // Still no module? it's an error if(!$this->module) { $this->error = 'msg_module_is_not_exists'; - $this->httpStatusCode = '404'; + $this->httpStatusCode = 404; return true; } @@ -415,26 +296,17 @@ class ModuleHandler extends Handler * */ public function procModule() { - $oModuleModel = ModuleModel::getInstance(); - $display_mode = Mobile::isFromMobilePhone() ? 'mobile' : 'view'; + // Set the display mode for the current device type. + $this->is_mobile = Mobile::isFromMobilePhone(); // If error occurred while preparation, return a message instance if($this->error) { - self::_setInputErrorToContext(); - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - if($this->httpStatusCode) - { - $oMessageObject->setHttpStatusCode($this->httpStatusCode); - } - return $oMessageObject; + return self::_createErrorMessage(-1, $this->error, $this->httpStatusCode); } // Get action information with conf/module.xml - $xml_info = $oModuleModel->getModuleActionXml($this->module); + $xml_info = ModuleModel::getModuleActionXml($this->module); // If not installed yet, modify act if($this->module == "install") @@ -454,19 +326,7 @@ class ModuleHandler extends Handler // still no act means error if(!$this->act) { - $this->error = 'msg_module_is_not_exists'; - $this->httpStatusCode = '404'; - - self::_setInputErrorToContext(); - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - if($this->httpStatusCode) - { - $oMessageObject->setHttpStatusCode($this->httpStatusCode); - } - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_module_is_not_exists', 404); } // get type, kind @@ -500,12 +360,7 @@ class ModuleHandler extends Handler if(!in_array(strtoupper($_SERVER['REQUEST_METHOD']), $allowedMethodList)) { - $this->error = 'msg_invalid_request'; - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_method_not_allowed', 405); } } @@ -514,13 +369,7 @@ class ModuleHandler extends Handler { if($xml_info->action->{$this->act} && $xml_info->action->{$this->act}->check_csrf !== 'false' && !checkCSRF()) { - $this->_setInputErrorToContext(); - $this->error = 'msg_invalid_request'; - $oMessageObject = ModuleHandler::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_security_violation'); } } @@ -532,7 +381,7 @@ class ModuleHandler extends Handler $logged_info = Context::get('logged_info'); // if(type == view, and case for using mobilephone) - if($type == "view" && Mobile::isFromMobilePhone() && Context::isInstalled()) + if($type == "view" && $this->is_mobile && Context::isInstalled()) { $orig_type = "view"; $type = "mobile"; @@ -553,16 +402,7 @@ class ModuleHandler extends Handler if(!is_object($oModule)) { - self::_setInputErrorToContext(); - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - if($this->httpStatusCode) - { - $oMessageObject->setHttpStatusCode($this->httpStatusCode); - } - return $oMessageObject; + return self::_createErrorMessage(-1, $this->error, $this->httpStatusCode); } // If there is no such action in the module object @@ -570,24 +410,14 @@ class ModuleHandler extends Handler { if(!Context::isInstalled()) { - self::_setInputErrorToContext(); - $this->error = 'msg_invalid_request'; - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - if($this->httpStatusCode) - { - $oMessageObject->setHttpStatusCode($this->httpStatusCode); - } - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_invalid_request'); } // 1. Look for the module with action name if(preg_match('/^([a-z]+)([A-Z])([a-z0-9\_]+)(.*)$/', $this->act, $matches)) { $module = strtolower($matches[2] . $matches[3]); - $xml_info = $oModuleModel->getModuleActionXml($module); + $xml_info = ModuleModel::getModuleActionXml($module); if($xml_info->action->{$this->act} && ($this->module == 'admin' || $xml_info->action->{$this->act}->standalone != 'false')) { @@ -600,19 +430,13 @@ class ModuleHandler extends Handler } else { - $this->error = 'msg_invalid_request'; - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_invalid_request'); } } if(empty($forward->module)) { - $forward = $oModuleModel->getActionForward($this->act); + $forward = ModuleModel::getActionForward($this->act); } if(!empty($forward->module)) @@ -627,20 +451,14 @@ class ModuleHandler extends Handler Context::addMetaTag('robots', 'noindex'); } - $xml_info = $oModuleModel->getModuleActionXml($forward->module); + $xml_info = ModuleModel::getModuleActionXml($forward->module); // Protect admin action - if(($this->module == 'admin' || $kind == 'admin') && !$oModuleModel->getGrant($forward, $logged_info)->root) + if(($this->module == 'admin' || $kind == 'admin') && !ModuleModel::getGrant($forward, $logged_info)->root) { - if($this->module == 'admin' || empty($xml_info->permission->{$this->act})) + if($this->module == 'admin' || empty($xml_info->action->{$this->act}->permission->target)) { - self::_setInputErrorToContext(); - $this->error = 'admin.msg_is_not_administrator'; - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - return $oMessageObject; + return self::_createErrorMessage(-1, 'admin.msg_is_not_administrator'); } } @@ -661,12 +479,7 @@ class ModuleHandler extends Handler if(!in_array(strtoupper($_SERVER['REQUEST_METHOD']), $allowedMethodList)) { - $this->error = 'msg_security_violation'; - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_method_not_allowed', 405); } } @@ -675,17 +488,11 @@ class ModuleHandler extends Handler { if($xml_info->action->{$this->act} && $xml_info->action->{$this->act}->check_csrf !== 'false' && !checkCSRF()) { - $this->_setInputErrorToContext(); - $this->error = 'msg_security_violation'; - $oMessageObject = ModuleHandler::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(); - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_security_violation'); } } - if($type == "view" && Mobile::isFromMobilePhone()) + if($type == "view" && $this->is_mobile) { $orig_type = "view"; $type = "mobile"; @@ -705,16 +512,7 @@ class ModuleHandler extends Handler if(!is_object($oModule)) { - self::_setInputErrorToContext(); - $oMessageObject = self::getModuleInstance('message', $display_mode); - $oMessageObject->setError(-1); - $oMessageObject->setMessage('msg_module_is_not_exists'); - $oMessageObject->dispMessage(); - if($this->httpStatusCode) - { - $oMessageObject->setHttpStatusCode($this->httpStatusCode); - } - return $oMessageObject; + return self::_createErrorMessage(-1, 'msg_module_is_not_exists', 404); } // Admin page layout @@ -743,7 +541,7 @@ class ModuleHandler extends Handler if(!empty($ruleset)) { $rulesetModule = !empty($forward->module) ? $forward->module : $this->module; - $rulesetFile = $oModuleModel->getValidatorFilePath($rulesetModule, $ruleset, $this->mid); + $rulesetFile = ModuleModel::getValidatorFilePath($rulesetModule, $ruleset, $this->mid); if(!empty($rulesetFile)) { if($_SESSION['XE_VALIDATOR_ERROR_LANG']) @@ -877,10 +675,135 @@ class ModuleHandler extends Handler } /** - * set error message to Session. + * Check the value of $document_srl. This method is called during init(). + * + * @return object|false + */ + protected function _checkDocumentSrl() + { + // Get the module that the document belongs to. + $module_info = ModuleModel::getModuleInfoByDocumentSrl($this->document_srl); + if($module_info) + { + // Compare the current mid to the module that the document belongs to. + if(!$this->mid || ($this->mid !== $module_info->mid)) + { + // If this is a GET request, redirect to the correct mid. + if(Context::getRequestMethod() === 'GET') + { + Context::setCacheControl(0); + header('Location: ' . getNotEncodedUrl('', 'mid', $module_info->mid, 'document_srl', $this->document_srl), true, 301); + return false; + } + // If this is NOT a GET request, don't redirect. Just overwrite the mid for the current request. + else + { + $this->mid = $module_info->mid; + Context::set('mid', $this->mid); + } + } + + // Remove module info if a different module has already been selected for the current request. + if($this->module && $module_info->module !== $this->module) + { + $module_info = null; + } + } + + // Block access to secret or temporary documents. + if(Context::getRequestMethod() === 'GET') + { + $oDocument = DocumentModel::getDocument($this->document_srl); + if($oDocument->isExists()) + { + $this->httpStatusCode = 404; + } + elseif(!$oDocument->isAccessible()) + { + $this->httpStatusCode = 403; + } + } + + // Return the module info for further processing. + return $module_info; + } + + /** + * Set SEO information to Context. + * + * @param object $module_info + * @param object $site_module_info + */ + protected function _setModuleSEOInfo($module_info, $site_module_info) + { + // Set the browser title. + if ($module_info->mid == $site_module_info->mid) + { + $seo_title = config('seo.main_title') ?: '$SITE_TITLE - $SITE_SUBTITLE'; + } + else + { + $seo_title = config('seo.subpage_title') ?: '$SITE_TITLE - $SUBPAGE_TITLE'; + } + ModuleController::getInstance()->replaceDefinedLangCode($seo_title); + Context::setBrowserTitle($seo_title, array( + 'site_title' => Context::getSiteTitle(), + 'site_subtitle' => Context::getSiteSubtitle(), + 'subpage_title' => $module_info->browser_title, + 'page' => Context::get('page') ?: 1, + )); + + // Set meta keywords. + $module_config = ModuleModel::getModuleConfig('module'); + if ($module_info->meta_keywords) + { + Context::addMetaTag('keywords', $module_info->meta_keywords); + } + elseif ($site_module_info->settings->meta_keywords) + { + Context::addMetaTag('keywords', $site_module_info->settings->meta_keywords); + } + elseif ($module_config->meta_keywords) + { + Context::addMetaTag('keywords', $module_config->meta_keywords); + } + + // Set meta description. + if ($module_info->meta_description) + { + Context::addMetaTag('description', $module_info->meta_description); + } + elseif ($site_module_info->settings->meta_description) + { + Context::addMetaTag('description', $site_module_info->settings->meta_description); + } + elseif($module_config->meta_description) + { + Context::addMetaTag('description', $module_config->meta_description); + } + } + + /** + * Save input values to session so that they can be recovered after returning to the previous form. + * * @return void - * */ - public static function _setInputErrorToContext() + */ + protected static function _setInputValueToSession() + { + $requestVars = getDestroyXeVars(Context::getRequestVars()); + unset($requestVars->act, $requestVars->mid, $requestVars->vid); + foreach($requestVars as $key => $value) + { + $_SESSION['INPUT_ERROR'][$key] = $value; + } + } + + /** + * Get previous error information and restore it to Context so that it is available to templates. + * + * @return void + */ + protected static function _setInputErrorToContext() { if($_SESSION['XE_VALIDATOR_ERROR'] && !Context::get('XE_VALIDATOR_ERROR')) { @@ -914,7 +837,7 @@ class ModuleHandler extends Handler * clear error message to Session. * @return void * */ - public static function _clearErrorSession() + protected static function _clearErrorSession() { unset($_SESSION['XE_VALIDATOR_ERROR']); unset($_SESSION['XE_VALIDATOR_MESSAGE']); @@ -925,17 +848,25 @@ class ModuleHandler extends Handler } /** - * occured error when, set input values to session. - * @return void - * */ - public static function _setInputValueToSession() + * Create a message module instance with an error message. + */ + protected static function _createErrorMessage($error, $message, $status_code = 403, $location = null) { - $requestVars = Context::getRequestVars(); - unset($requestVars->act, $requestVars->mid, $requestVars->vid, $requestVars->success_return_url, $requestVars->error_return_url, $requestVars->xe_validator_id); - foreach($requestVars AS $key => $value) + $display_mode = Mobile::isFromMobilePhone() ? 'mobile' : 'view'; + if (!$location) { - $_SESSION['INPUT_ERROR'][$key] = $value; + $backtrace = debug_backtrace(false); + $caller = array_shift($backtrace); + $location = $caller['file'] . ':' . $caller['line']; } + + self::_setInputErrorToContext(); + $oMessageObject = self::getModuleInstance('message', $display_mode); + $oMessageObject->setError($error); + $oMessageObject->setMessage($message); + $oMessageObject->setHttpStatusCode($status_code ?: 403); + $oMessageObject->dispMessage('', $location); + return $oMessageObject; } /** @@ -945,11 +876,14 @@ class ModuleHandler extends Handler * */ public function displayContent($oModule = NULL) { + // Set the display mode for the current device type. + $this->is_mobile = Mobile::isFromMobilePhone(); + // If the module is not set or not an object, set error if(!$oModule || !is_object($oModule)) { $this->error = 'msg_module_is_not_exists'; - $this->httpStatusCode = '404'; + $this->httpStatusCode = 404; } // If connection to DB has a problem even though it's not install module, set error @@ -1014,11 +948,7 @@ class ModuleHandler extends Handler if($this->error) { // display content with message module instance - $type = Mobile::isFromMobilePhone() ? 'mobile' : 'view'; - $oMessageObject = self::getModuleInstance('message', $type); - $oMessageObject->setError(-1); - $oMessageObject->setMessage($this->error); - $oMessageObject->dispMessage(null, $oModule->get('rx_error_location')); + $oMessageObject = self::_createErrorMessage(-1, $this->error, $this->httpStatusCode, $oModule->get('rx_error_location')); // display Error Page if(!in_array($oMessageObject->getHttpStatusCode(), array(200, 403))) @@ -1032,8 +962,8 @@ class ModuleHandler extends Handler $oModule->setTemplatePath($oMessageObject->getTemplatePath()); $oModule->setTemplateFile($oMessageObject->getTemplateFile()); $oModule->setHttpStatusCode($oMessageObject->getHttpStatusCode()); - // Otherwise, set message instance as the target module } + // Otherwise, set message instance as the target module else { $oModule = $oMessageObject; @@ -1043,7 +973,7 @@ class ModuleHandler extends Handler } // Check if layout_srl exists for the module - $viewType = (Mobile::isFromMobilePhone()) ? 'M' : 'P'; + $viewType = $this->is_mobile ? 'M' : 'P'; if($viewType === 'M') { $layout_srl = $oModule->module_info->mlayout_srl; @@ -1234,8 +1164,7 @@ class ModuleHandler extends Handler return new BaseObject(); } - $oModuleModel = ModuleModel::getInstance(); - $triggers = $oModuleModel->getTriggers($trigger_name, $called_position); + $triggers = ModuleModel::getTriggers($trigger_name, $called_position); if(!$triggers) { $triggers = array(); @@ -1290,7 +1219,7 @@ class ModuleHandler extends Handler unset($oModule); } - $trigger_functions = $oModuleModel->getTriggerFunctions($trigger_name, $called_position); + $trigger_functions = ModuleModel::getTriggerFunctions($trigger_name, $called_position); foreach($trigger_functions as $item) { try diff --git a/classes/module/ModuleObject.class.php b/classes/module/ModuleObject.class.php index eba49f3cc..efa543e20 100644 --- a/classes/module/ModuleObject.class.php +++ b/classes/module/ModuleObject.class.php @@ -251,10 +251,10 @@ class ModuleObject extends BaseObject if(Context::get('logged_info')->is_admin !== 'Y') { // Get privileges(granted) information for target module by of module.xml - if(($permission_check = $this->xml_info->permission_check->{$this->act}) && $permission_check->key) + if(($permission = $this->xml_info->action->{$this->act}->permission) && $permission->check_var) { // Check parameter - if(empty($check_module_srl = trim(Context::get($permission_check->key)))) + if(empty($check_module_srl = trim(Context::get($permission->check_var)))) { return false; } @@ -277,7 +277,7 @@ class ModuleObject extends BaseObject foreach($check_module_srl as $target_srl) { // Get privileges(granted) information of current user for target module - if(($grant = ModuleModel::getInstance()->getPrivilegesBySrl($target_srl, $permission_check->type)) === false) + if(($grant = ModuleModel::getInstance()->getPrivilegesBySrl($target_srl, $permission->check_type)) === false) { return false; } @@ -335,10 +335,9 @@ class ModuleObject extends BaseObject } // Get privileges(granted) information of the member for current module - $oModuleModel = ModuleModel::getInstance(); if(!$grant) { - $grant = $oModuleModel->getGrant($this->module_info, $member_info, $this->xml_info); + $grant = ModuleModel::getGrant($this->module_info, $member_info, $this->xml_info); } // If an administrator, Pass @@ -348,7 +347,7 @@ class ModuleObject extends BaseObject } // Get permission types(guest, member, manager, root) of the currently requested action - $permission = $this->xml_info->permission->{$this->act}; + $permission = $this->xml_info->action->{$this->act}->permission->target ?: $this->xml_info->permission->{$this->act}; // If admin action, set default permission if(empty($permission) && stripos($this->act, 'admin') !== false) @@ -381,17 +380,17 @@ class ModuleObject extends BaseObject if(Context::get('is_logged') && isset($type[2])) { // Manager privilege of the member is found by search all modules, Pass - if($type[2] == 'all' && $oModuleModel->findManagerPrivilege($member_info) !== false) + if($type[2] == 'all' && ModuleModel::findManagerPrivilege($member_info) !== false) { return true; } // Manager privilege of the member is found by search same module as this module, Pass - elseif($type[2] == 'same' && $oModuleModel->findManagerPrivilege($member_info, $this->module) !== false) + elseif($type[2] == 'same' && ModuleModel::findManagerPrivilege($member_info, $this->module) !== false) { return true; } // Manager privilege of the member is found by search same module as the module, Pass - elseif($oModuleModel->findManagerPrivilege($member_info, $type[2]) !== false) + elseif(ModuleModel::findManagerPrivilege($member_info, $type[2]) !== false) { return true; } @@ -650,7 +649,6 @@ class ModuleObject extends BaseObject // Set module skin if(isset($this->module_info->skin) && $this->module_info->module === $this->module && strpos($this->act, 'Admin') === false) { - $oModuleModel = ModuleModel::getInstance(); $skin_type = $is_mobile ? 'M' : 'P'; $skin_key = $is_mobile ? 'mskin' : 'skin'; $skin_dir = $is_mobile ? 'm.skins' : 'skins'; @@ -662,7 +660,7 @@ class ModuleObject extends BaseObject { if($module_skin === '/USE_DEFAULT/') { - $module_skin = $oModuleModel->getModuleDefaultSkin($this->module, $skin_type); + $module_skin = ModuleModel::getModuleDefaultSkin($this->module, $skin_type); $this->module_info->{$skin_key} = $module_skin; } if($module_skin === '/USE_RESPONSIVE/') @@ -671,7 +669,7 @@ class ModuleObject extends BaseObject $module_skin = $this->module_info->skin ?: '/USE_DEFAULT/'; if($module_skin === '/USE_DEFAULT/') { - $module_skin = $oModuleModel->getModuleDefaultSkin($this->module, 'P'); + $module_skin = ModuleModel::getModuleDefaultSkin($this->module, 'P'); } } if(!is_dir(sprintf('%s%s/%s', $this->module_path, $skin_dir, $module_skin))) @@ -682,7 +680,7 @@ class ModuleObject extends BaseObject } // Set skin variable - $oModuleModel->syncSkinInfoToModuleInfo($this->module_info); + ModuleModel::syncSkinInfoToModuleInfo($this->module_info); Context::set('module_info', $this->module_info); } diff --git a/common/defaults/config.php b/common/defaults/config.php index abc96e02a..7972452de 100644 --- a/common/defaults/config.php +++ b/common/defaults/config.php @@ -50,6 +50,7 @@ return array( 'http_port' => null, 'https_port' => null, 'ssl' => 'none', + 'rewrite' => 1, ), 'session' => array( 'delay' => false, diff --git a/common/framework/parsers/moduleactionparser.php b/common/framework/parsers/moduleactionparser.php new file mode 100644 index 000000000..9b5260cfe --- /dev/null +++ b/common/framework/parsers/moduleactionparser.php @@ -0,0 +1,257 @@ + '[0-9]+', + 'float' => '[0-9]+(?:\.[0-9]+)?', + 'alpha' => '[a-zA-Z]+', + 'alnum' => '[a-zA-Z0-9]+', + 'hex' => '[0-9a-f]+', + 'word' => '[a-zA-Z0-9_]+', + 'any' => '[^/]+', + 'delete' => '[^/]+', + ); + + /** + * Load an XML file. + * + * @param string $filename + * @return object|false + */ + public static function loadXML(string $filename) + { + // Load the XML file. + $xml = simplexml_load_string(file_get_contents($filename)); + if ($xml === false) + { + return false; + } + + // Get the current language. + $lang = \Context::getLangType() ?: 'en'; + + // Initialize the module definition. + $info = new \stdClass; + $info->admin_index_act = ''; + $info->default_index_act = ''; + $info->setup_index_act = ''; + $info->simple_setup_index_act = ''; + $info->route = new \stdClass; + $info->route->GET = []; + $info->route->POST = []; + $info->action = new \stdClass; + $info->grant = new \stdClass; + $info->menu = new \stdClass; + $info->error_handlers = []; + + // Parse grants. + foreach ($xml->grants->grant ?: [] as $grant) + { + $grant_info = new \stdClass; + $grant_info->title = self::_getElementsByLang($grant, 'title', $lang); + $grant_info->default = trim($grant['default']); + $grant_name = trim($grant['name']); + $info->grant->{$grant_name} = $grant_info; + } + + // Parse menus. + foreach ($xml->menus->menu ?: [] as $menu) + { + $menu_info = new \stdClass; + $menu_info->title = self::_getElementsByLang($menu, 'title', $lang); + $menu_info->index = null; + $menu_info->acts = array(); + $menu_info->type = trim($menu['type']); + $menu_name = trim($menu['name']); + $info->menu->{$menu_name} = $menu_info; + } + + // Parse actions. + foreach ($xml->actions->action ?: [] as $action) + { + // Parse permissions. + $action_name = trim($action['name']); + $permission = trim($action['permission']); + $permission_info = (object)['target' => '', 'check_var' => '', 'check_type' => '']; + if ($permission) + { + $permission_info->target = $permission; + $permission_info->check_var = trim($action['check_var']) ?: trim($action['check-var']); + $permission_info->check_type = trim($action['check_type']) ?: trim($action['check-type']); + } + + // Parse routes. + $route_attr = trim($action['route']); + $route_tags = $action->route ?: []; + $method = trim($action['method']); + $route_arg = []; + if ($route_attr || count($route_tags)) + { + $methods = $method ? explode('|', strtoupper($method)) : (starts_with('proc', $action_name) ? ['POST'] : ['GET']); + $routes = $route_attr ? array_map(function($route) { + return ['route' => trim($route), 'priority' => 0]; + }, explode_with_escape('|', $route_attr)) : array(); + foreach ($route_tags as $route_tag) + { + $routes[] = ['route' => trim($route_tag['route']), 'priority' => intval($route_tag['priority'] ?: 0)]; + } + foreach ($routes as $route) + { + $route_info = self::analyzeRoute($route); + $route_arg[$route_info->route] = ['priority' => intval($route_info->priority), 'vars' => $route_info->vars]; + foreach ($methods as $method) + { + $info->route->{$method}[$route_info->regexp] = $action_name; + } + } + } + + // Parse other information about this action. + $action_info = new \stdClass; + $action_info->type = trim($action['type']); + $action_info->grant = trim($action['grant']) ?: 'guest'; + $action_info->permission = $permission_info; + $action_info->ruleset = trim($action['ruleset']); + $action_info->method = $method; + $action_info->route = $route_arg; + $action_info->standalone = trim($action['standalone']) === 'false' ? 'false' : 'true'; + $action_info->check_csrf = (trim($action['check_csrf']) ?: trim($action['check-csrf'])) === 'false' ? 'false' : 'true'; + $action_info->meta_noindex = (trim($action['meta_noindex']) ?: trim($action['meta-noindex'])) === 'true' ? 'true' : 'false'; + $action_info->global_route = (trim($action['global_route']) ?: trim($action['global-route'])) === 'true' ? 'true' : 'false'; + $action_info->use_ssl = (trim($action['use_ssl']) ?: trim($action['use-ssl'])) === 'true' ? 'true' : 'false'; + $info->action->{$action_name} = $action_info; + + // Set the menu name and index settings. + $menu_name = trim($action['menu_name']); + if ($menu_name) + { + $info->menu->{$menu_name}->acts[] = $action_name; + if (toBool($action['menu_index'])) + { + $info->menu->{$menu_name}->index = $action_name; + } + } + if (toBool($action['index'])) + { + $info->default_index_act = $action_name; + } + if (toBool($action['admin_index'])) + { + $info->admin_index_act = $action_name; + } + if (toBool($action['setup_index'])) + { + $info->setup_index_act = $action_name; + } + if (toBool($action['simple_setup_index'])) + { + $info->simple_setup_index_act = $action_name; + } + + // Set error handler settings. + $error_handlers = explode(',', trim($action['error_handlers']) ?: trim($action['error-handlers'])); + foreach ($error_handlers as $error_handler) + { + if (intval($error_handler) > 200) + { + $info->error_handlers[intval($error_handler)] = $action_name; + } + } + } + + // Parse permissions not defined in the section. + foreach ($xml->permissions->permission ?: [] as $permission) + { + $action_name = trim($permission['action']); + if (isset($info->action->{$action_name})) + { + $info->action->{$action_name}->permission->target = trim($permission['target']); + $info->action->{$action_name}->permission->check_var = trim($permission['check_var']) ?: trim($permission['check-var']); + $info->action->{$action_name}->permission->check_type = trim($permission['check_type']) ?: trim($permission['check-type']); + } + } + + // Return the complete result. + return $info; + } + + /** + * Convert route definition into a regular expression. + * + * @param array $route + * @return object + */ + public static function analyzeRoute(array $route) + { + // Replace variables in the route definition into appropriate regexp. + $var_regexp = '#\\$([a-zA-Z0-9_]+)(?::(' . implode('|', array_keys(self::$_shortcuts)) . '))?#'; + $vars = array(); + $regexp = preg_replace_callback($var_regexp, function($match) use(&$vars) { + if (isset($match[2])) + { + $var_type = $match[2]; + $var_pattern = self::$_shortcuts[$match[2]]; + } + else + { + $var_type = ends_with('_srl', $match[1]) ? 'int' : 'any'; + $var_pattern = self::$_shortcuts[$var_type]; + } + $named_group = '(?P<' . $match[1] . '>' . $var_pattern . ')'; + $vars[$match[1]] = $var_type; + return $named_group; + }, $route['route']); + + // Anchor the regexp at both ends. + $regexp = '#^' . strtr($regexp, ['#' => '\\#']) . '$#u'; + + // Return the regexp and variable list. + $result = new \stdClass; + $result->route = preg_replace_callback($var_regexp, function($match) { + return '$' . ((isset($match[2]) && $match[2] === 'delete') ? ($match[1] . ':' . $match[2]) : $match[1]); + }, $route['route']); + $result->priority = $route['priority'] ?: 0; + $result->regexp = $regexp; + $result->vars = $vars; + return $result; + } + + /** + * Get child elements that match a language. + * + * @param SimpleXMLElement $parent + * @param string $tag_name + * @param string $lang + * @return string + */ + protected static function _getElementsByLang(\SimpleXMLElement $parent, string $tag_name, string $lang): string + { + // If there is a child element that matches the language, return it. + foreach ($parent->{$tag_name} as $child) + { + $attribs = $child->attributes('xml', true); + if (strval($attribs['lang']) === $lang) + { + return trim($child); + } + } + + // Otherwise, return the first child element. + foreach ($parent->{$tag_name} as $child) + { + return trim($child); + } + + // If there are no child elements, return an empty string. + return ''; + } +} diff --git a/common/framework/parsers/moduleinfoparser.php b/common/framework/parsers/moduleinfoparser.php new file mode 100644 index 000000000..4b58745d1 --- /dev/null +++ b/common/framework/parsers/moduleinfoparser.php @@ -0,0 +1,121 @@ +title = self::_getElementsByLang($xml, 'title', $lang); + $info->description = self::_getElementsByLang($xml, 'description', $lang); + $info->version = trim($xml->version); + $info->homepage = trim($xml->homepage); + $info->category = trim($xml->category) ?: 'service'; + $info->date = date('Ymd', strtotime($xml->date . 'T12:00:00Z')); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link']); + $info->author = array(); + + foreach ($xml->author as $author) + { + $author_info = new \stdClass; + $author_info->name = self::_getElementsByLang($author, 'name', $lang); + $author_info->email_address = trim($author['email_address']); + $author_info->homepage = trim($author['link']); + $info->author[] = $author_info; + } + } + + // Parse version 0.1 + else + { + $info->title = self::_getElementsByLang($xml, 'title', $lang); + $info->description = self::_getElementsByLang($xml->author, 'description', $lang); + $info->version = trim($xml['version']); + $info->homepage = trim($xml->homepage); + $info->category = trim($xml['category']) ?: 'service'; + $info->date = date('Ymd', strtotime($xml->author['date'] . 'T12:00:00Z')); + $info->license = trim($xml->license); + $info->license_link = trim($xml->license['link']); + $info->author = array(); + + foreach ($xml->author as $author) + { + $author_info = new \stdClass; + $author_info->name = self::_getElementsByLang($author, 'name', $lang); + $author_info->email_address = trim($author['email_address']); + $author_info->homepage = trim($author['link']); + $info->author[] = $author_info; + } + } + + // Add information about actions. + $action_info = ModuleActionParser::loadXML(strtr($filename, ['info.xml' => 'module.xml'])); + $info->admin_index_act = $action_info->admin_index_act; + $info->default_index_act = $action_info->default_index_act; + $info->setup_index_act = $action_info->setup_index_act; + $info->simple_setup_index_act = $action_info->simple_setup_index_act; + $info->error_handlers = $action_info->error_handlers ?: []; + + // Return the complete result. + return $info; + } + + /** + * Get child elements that match a language. + * + * @param SimpleXMLElement $parent + * @param string $tag_name + * @param string $lang + * @return string + */ + protected static function _getElementsByLang(\SimpleXMLElement $parent, string $tag_name, string $lang): string + { + // If there is a child element that matches the language, return it. + foreach ($parent->{$tag_name} as $child) + { + $attribs = $child->attributes('xml', true); + if (strval($attribs['lang']) === $lang) + { + return trim($child); + } + } + + // Otherwise, return the first child element. + foreach ($parent->{$tag_name} as $child) + { + return trim($child); + } + + // If there are no child elements, return an empty string. + return ''; + } +} diff --git a/common/framework/router.php b/common/framework/router.php new file mode 100644 index 000000000..75bb2e1ac --- /dev/null +++ b/common/framework/router.php @@ -0,0 +1,570 @@ + array( + 'regexp' => '#^(?[0-9]+)$#', + 'vars' => ['document_srl' => 'int'], + 'priority' => 0, + ), + '$mid' => array( + 'regexp' => '#^(?[a-zA-Z0-9_-]+)/?$#', + 'vars' => ['mid' => 'any'], + 'priority' => 0, + ), + '$act' => array( + 'regexp' => '#^(?rss|atom)$#', + 'vars' => ['act' => 'word'], + 'priority' => 0, + ), + '$mid/$document_srl' => array( + 'regexp' => '#^(?[a-zA-Z0-9_-]+)/(?[0-9]+)$#', + 'vars' => ['mid' => 'any', 'document_srl' => 'int'], + 'priority' => 30, + ), + '$mid/category/$category' => array( + 'regexp' => '#^(?[a-zA-Z0-9_-]+)/category/(?[0-9]+)$#', + 'vars' => ['mid' => 'any', 'category' => 'int'], + 'priority' => 10, + ), + '$mid/entry/$entry' => array( + 'regexp' => '#^(?[a-zA-Z0-9_-]+)/entry/(?[^/]+)$#', + 'vars' => ['mid' => 'any', 'entry' => 'any'], + 'priority' => 0, + ), + '$mid/$act' => array( + 'regexp' => '#^(?[a-zA-Z0-9_-]+)/(?rss|atom|api)$#', + 'vars' => ['mid' => 'any', 'act' => 'word'], + 'priority' => 20, + ), + 'files/download/$file_srl/$file_key/$filename' => array( + 'regexp' => '#^files/download/(?[0-9]+)/(?[a-zA-Z0-9_-]+)/(?[^/]+)$#', + 'vars' => ['file_srl' => 'int', 'file_key' => 'any', 'filename' => 'any'], + 'extra_vars' => ['act' => 'procFileOutput'], + 'priority' => 0, + ), + ); + + /** + * List of legacy modules whose URLs should not be shortened. + */ + protected static $_except_modules = array( + 'socialxe' => true, + ); + + /** + * Internal cache for module and route information. + */ + protected static $_action_cache_prefix = array(); + protected static $_action_cache_module = array(); + protected static $_global_forwarded_cache = array(); + protected static $_internal_forwarded_cache = array(); + protected static $_route_cache = array(); + + /** + * Return the currently configured rewrite level. + * + * 0 = None + * 1 = XE-compatible rewrite rules only + * 2 = Full rewrite support + * + * @return int + */ + public static function getRewriteLevel(): int + { + $level = Config::get('url.rewrite'); + if ($level === null) + { + $level = Config::get('use_rewrite') ? 1 : 0; + } + return intval($level); + } + + /** + * Extract request arguments from the current URL. + * + * @param string $method + * @param string $url + * @param int $rewrite_level + * @return object + */ + public static function parseURL(string $method, string $url, int $rewrite_level) + { + // Get the local part of the current URL. + if (starts_with(\RX_BASEURL, $url)) + { + $url = substr($url, strlen(\RX_BASEURL)); + } + + // Prepare the return object. + $result = new \stdClass; + $result->status = 200; + $result->url = ''; + $result->module = ''; + $result->mid = ''; + $result->act = ''; + $result->forwarded = false; + $result->args = array(); + + // Separate additional arguments from the URL. + $args = array(); + $argstart = strpos($url, '?'); + if ($argstart !== false) + { + @parse_str(substr($url, $argstart + 1), $args); + $url = substr($url, 0, $argstart); + } + + // Decode the URL into plain UTF-8. + $url = $result->url = urldecode($url); + if ($url === '') + { + return $result; + } + if (function_exists('mb_check_encoding') && !mb_check_encoding($url, 'UTF-8')) + { + $result->status = 404; + return $result; + } + + // Try to detect the prefix. This might be $mid. + if ($rewrite_level >= 2 && preg_match('#^([a-zA-Z0-9_-]+)(?:/(.*))?#s', $url, $matches)) + { + // Separate the prefix and the internal part of the URL. + $prefix = $matches[1]; + $internal_url = $matches[2] ?? ''; + $prefix_type = 'mid'; + + // Find the module associated with this prefix. + $action_info = self::_getActionInfoByPrefix($prefix, $module_name = ''); + if ($action_info === false) + { + $action_info = self::_getActionInfoByModule($prefix); + if ($action_info !== false) + { + $module_name = $prefix; + $prefix_type = 'module'; + } + } + + // If a module is found, try its routes. + if ($action_info) + { + // Try the list of routes defined by the module. + foreach ($action_info->route->{$method} as $regexp => $action) + { + if (preg_match($regexp, $internal_url, $matches)) + { + $matches = array_filter($matches, 'is_string', \ARRAY_FILTER_USE_KEY); + $allargs = array_merge($args, $matches, [$prefix_type => $prefix, 'act' => $action]); + $result->module = $module_name; + $result->mid = $prefix_type === 'mid' ? $prefix : ''; + $result->act = $action; + $result->args = $allargs; + return $result; + } + } + + // Check other modules. + if ($prefix_type === 'mid') + { + $forwarded_routes = self::_getForwardedRoutes('internal'); + foreach ($forwarded_routes[$method] ?: [] as $regexp => $action) + { + if (preg_match($regexp, $internal_url, $matches)) + { + $matches = array_filter($matches, 'is_string', \ARRAY_FILTER_USE_KEY); + $allargs = array_merge($args, $matches, [$prefix_type => $prefix, 'act' => $action[1]]); + $result->module = $action[0]; + $result->mid = $prefix; + $result->act = $action[1]; + $result->forwarded = true; + $result->args = $allargs; + return $result; + } + } + } + + // Try the generic mid/act pattern. + if (preg_match('#^[a-zA-Z0-9_]+$#', $internal_url)) + { + $allargs = array_merge($args, [$prefix_type => $prefix, 'act' => $internal_url]); + $result->module = $module_name; + $result->mid = $prefix_type === 'mid' ? $prefix : ''; + $result->act = $internal_url; + $result->forwarded = true; + $result->args = $allargs; + return $result; + } + + // If the module defines a 404 error handler, call it. + if ($internal_url && isset($action_info->error_handlers[404])) + { + $allargs = array_merge($args, [$prefix_type => $prefix, 'act' => $action_info->error_handlers[404]]); + $result->module = $module_name; + $result->mid = $prefix_type === 'mid' ? $prefix : ''; + $result->act = $action_info->error_handlers[404]; + $result->forwarded = false; + $result->args = $allargs; + return $result; + } + } + } + + // Try registered global routes. + if ($rewrite_level >= 2) + { + $global_routes = self::_getForwardedRoutes('global'); + foreach ($global_routes[$method] ?: [] as $regexp => $action) + { + if (preg_match($regexp, $url, $matches)) + { + $matches = array_filter($matches, 'is_string', \ARRAY_FILTER_USE_KEY); + $allargs = array_merge($args, $matches, ['act' => $action[1]]); + $result->module = $action[0]; + $result->act = $action[1]; + $result->forwarded = true; + $result->args = $allargs; + return $result; + } + } + } + + // Try XE-compatible global routes. + foreach (self::$_global_routes as $route_info) + { + if (preg_match($route_info['regexp'], $url, $matches)) + { + $matches = array_filter($matches, 'is_string', \ARRAY_FILTER_USE_KEY); + $allargs = array_merge($args, $matches, $route_info['extra_vars'] ?? []); + $result->module = $allargs['module'] ?? ''; + $result->mid = $allargs['mid'] ?: ''; + $result->act = $allargs['act'] ?: ''; + $result->forwarded = false; + $result->args = $allargs; + return $result; + } + } + + // If no pattern matches, return either an empty route or a 404 error. + $result->module = isset($args['module']) ? $args['module'] : ''; + $result->mid = isset($args['mid']) ? $args['mid'] : ''; + $result->act = isset($args['act']) ? $args['act'] : ''; + $result->args = $args; + if ($url === '' || $url === 'index.php') + { + $result->url = ''; + return $result; + } + else + { + $result->status = 404; + return $result; + } + } + + /** + * Create a URL for the given set of arguments. + * + * @param array $args + * @param int $rewrite_level + * @return string + */ + public static function getURL(array $args, int $rewrite_level): string + { + // If rewrite is turned off, just create a query string. + if ($rewrite_level == 0) + { + return 'index.php?' . http_build_query($args); + } + + // Cache the number of arguments and their keys. + $count = count($args); + $keys = array_keys($args); + + // If there are no arguments, return the URL of the main page. + if ($count == 0) + { + return ''; + } + + // If there is only one argument, try either $mid or $document_srl. + if ($rewrite_level >= 1 && $count == 1 && ($keys[0] === 'mid' || $keys[0] === 'document_srl')) + { + return urlencode($args[$keys[0]]); + } + + // If the list of keys is already cached, return the corresponding route. + $keys_sorted = $keys; sort($keys_sorted); + $keys_string = implode('.', $keys_sorted) . ':' . ($args['mid'] ?? '') . ':' . ($args['act'] ?? ''); + if (isset(self::$_route_cache[$rewrite_level][$keys_string])) + { + return self::_insertRouteVars(self::$_route_cache[$rewrite_level][$keys_string], $args); + } + + // Remove $mid and $act from arguments and work with the remainder. + $args2 = $args; unset($args2['module'], $args2['mid'], $args2['act']); + + // If $mid exists, try routes defined in the module. + if ($rewrite_level >= 2 && (isset($args['mid']) || isset($args['module']))) + { + // Get module action info. + if (isset($args['mid'])) + { + $action_info = self::_getActionInfoByPrefix($args['mid']); + $prefix_type = 'mid'; + } + elseif (isset($args['module'])) + { + $action_info = self::_getActionInfoByModule($args['module']); + $prefix_type = 'module'; + } + + // If there is no $act, use the default action. + $act = isset($args['act']) ? $args['act'] : $action_info->default_index_act; + + // Check if $act has any routes defined. + $action = $action_info->action->{$act} ?? null; + if ($action && $action->route) + { + $result = self::_getBestMatchingRoute($action->route, $args2); + if ($result !== false) + { + self::$_route_cache[$rewrite_level][$keys_string] = '$' . $prefix_type . '/' . $result . '$act:delete'; + return $args[$prefix_type] . '/' . self::_insertRouteVars($result, $args2); + } + } + + // Check other modules for $act. + if ($prefix_type === 'mid') + { + $forwarded_routes = self::_getForwardedRoutes('internal'); + if (isset($forwarded_routes['reverse'][$act])) + { + $result = self::_getBestMatchingRoute($forwarded_routes['reverse'][$act], $args2); + if ($result !== false) + { + self::$_route_cache[$rewrite_level][$keys_string] = '$' . $prefix_type . '/' . $result . '$act:delete'; + return $args[$prefix_type] . '/' . self::_insertRouteVars($result, $args2); + } + } + } + + // Try the generic mid/act pattern. + if ($prefix_type !== 'module' || !isset(self::$_except_modules[$args[$prefix_type]])) + { + self::$_route_cache[$rewrite_level][$keys_string] = '$' . $prefix_type . '/$act'; + return $args[$prefix_type] . '/' . $args['act'] . (count($args2) ? ('?' . http_build_query($args2)) : ''); + } + } + + // Try registered global routes. + if ($rewrite_level >= 2 && isset($args['act'])) + { + $global_routes = self::_getForwardedRoutes('global'); + if (isset($global_routes['reverse'][$args['act']])) + { + $result = self::_getBestMatchingRoute($global_routes['reverse'][$args['act']], $args2); + if ($result !== false) + { + self::$_route_cache[$rewrite_level][$keys_string] = $result . '$act:delete'; + return self::_insertRouteVars($result, $args2); + } + } + } + + // Try XE-compatible global routes. + if ($rewrite_level >= 1) + { + if (!isset($args['act']) || ($args['act'] === 'rss' || $args['act'] === 'atom')) + { + $result = self::_getBestMatchingRoute(self::$_global_routes, $args); + if ($result !== false) + { + self::$_route_cache[$rewrite_level][$keys_string] = $result; + return self::_insertRouteVars($result, $args); + } + } + } + + // If no route matches, just create a query string. + self::$_route_cache[$rewrite_level][$keys_string] = 'index.php'; + return 'index.php?' . http_build_query($args); + } + + /** + * Load and cache module action info. + * + * @param string $prefix + * @return object + */ + protected static function _getActionInfoByPrefix(string $prefix, string &$module_name = '') + { + if (isset(self::$_action_cache_prefix[$prefix])) + { + $module_name = self::$_action_cache_prefix[$prefix]; + return self::_getActionInfoByModule(self::$_action_cache_prefix[$prefix]) ?: false; + } + + $module_info = \ModuleModel::getModuleInfoByMid($prefix); + if ($module_info && $module_info->module) + { + $module_name = self::$_action_cache_prefix[$prefix] = $module_info->module; + return self::_getActionInfoByModule(self::$_action_cache_prefix[$prefix]) ?: false; + } + else + { + return self::$_action_cache_prefix[$prefix] = false; + } + } + + /** + * Load and cache module action info. + * + * @param string $prefix + * @return object + */ + protected static function _getActionInfoByModule(string $module) + { + if (isset(self::$_action_cache_module[$module])) + { + return self::$_action_cache_module[$module]; + } + + $action_info = \ModuleModel::getModuleActionXml($module); + return self::$_action_cache_module[$module] = $action_info ?: false; + } + + /** + * Get the list of routes that are registered for action-forward. + * + * @param string $type + * @return array + */ + protected static function _getForwardedRoutes(string $type): array + { + if ($type === 'internal' && count(self::$_internal_forwarded_cache)) + { + return self::$_internal_forwarded_cache; + } + if ($type === 'global' && count(self::$_global_forwarded_cache)) + { + return self::$_global_forwarded_cache; + } + + self::$_global_forwarded_cache['GET'] = array(); + self::$_global_forwarded_cache['POST'] = array(); + self::$_global_forwarded_cache['reverse'] = array(); + self::$_internal_forwarded_cache['GET'] = array(); + self::$_internal_forwarded_cache['POST'] = array(); + self::$_internal_forwarded_cache['reverse'] = array(); + + $action_forward = \ModuleModel::getActionForward(); + foreach ($action_forward as $action_name => $action_info) + { + if ($action_info->route_regexp) + { + foreach ($action_info->route_regexp as $regexp_info) + { + if ($action_info->global_route === 'Y') + { + self::$_global_forwarded_cache[$regexp_info[0]][$regexp_info[1]] = [$action_info->module, $action_name]; + } + else + { + self::$_internal_forwarded_cache[$regexp_info[0]][$regexp_info[1]] = [$action_info->module, $action_name]; + } + } + if ($action_info->global_route === 'Y') + { + self::$_global_forwarded_cache['reverse'][$action_name] = $action_info->route_config; + } + else + { + self::$_internal_forwarded_cache['reverse'][$action_name] = $action_info->route_config; + } + } + } + return $type === 'internal' ? self::$_internal_forwarded_cache : self::$_global_forwarded_cache; + } + + /** + * Find the best matching route for an array of variables. + * + * @param array $routes + * @param array $vars + * @return string|false + */ + protected static function _getBestMatchingRoute(array $routes, array $vars) + { + // If the action only has one route, select it. + if (count($routes) == 1) + { + $only_route = key($routes); + $matched_arguments = array_intersect_key($routes[$only_route]['vars'], $vars); + if (count($matched_arguments) !== count($routes[$only_route]['vars'])) + { + return false; + } + return $only_route; + } + + // If the action has multiple routes, select the one that matches the most arguments. + else + { + // Order the routes by the number of matched arguments. + $reordered_routes = array(); + foreach ($routes as $route => $route_vars) + { + $matched_arguments = array_intersect_key($route_vars['vars'], $vars); + if (count($matched_arguments) === count($route_vars['vars'])) + { + $reordered_routes[$route] = ($route_vars['priority'] * 1000) + count($matched_arguments); + } + } + if (!count($reordered_routes)) + { + return false; + } + arsort($reordered_routes); + $best_route = array_first_key($reordered_routes); + return $best_route; + } + } + + /** + * Insert variables into a route. + * + * @param string $route + * @param array $vars + * @return string + */ + protected static function _insertRouteVars(string $route, array $vars): string + { + // Replace variable placeholders with actual variable values. + $route = preg_replace_callback('#\\$([a-zA-Z0-9_]+)(:[a-z]+)?#i', function($match) use(&$vars) { + if (isset($vars[$match[1]])) + { + $replacement = urlencode($vars[$match[1]]); + unset($vars[$match[1]]); + return (isset($match[2]) && $match[2] === ':delete') ? '' : $replacement; + } + else + { + return ''; + } + }, $route); + + // Add a query string for the remaining arguments. + return $route . (count($vars) ? ('?' . http_build_query($vars)) : ''); + } +} diff --git a/common/lang/en.php b/common/lang/en.php index 5ec21ffc4..7ed9ed903 100644 --- a/common/lang/en.php +++ b/common/lang/en.php @@ -234,6 +234,7 @@ $lang->msg_invalid_document = 'Invalid Article Number'; $lang->msg_invalid_request = 'Invalid Request'; $lang->msg_invalid_password = 'The password you entered is incorrect.'; $lang->msg_security_violation = 'Security Violation'; +$lang->msg_method_not_allowed = 'This HTTP method is not allowed for this action.'; $lang->msg_feature_disabled = 'This feature is disabled.'; $lang->msg_error_occured = 'An error has occured.'; $lang->msg_not_founded = 'Cannot find the target.'; diff --git a/common/lang/ko.php b/common/lang/ko.php index f6ba251ec..d8074d82e 100644 --- a/common/lang/ko.php +++ b/common/lang/ko.php @@ -236,6 +236,7 @@ $lang->msg_invalid_document = '잘못된 문서번호입니다.'; $lang->msg_invalid_request = '잘못된 요청입니다.'; $lang->msg_invalid_password = '비밀번호가 올바르지 않습니다.'; $lang->msg_security_violation = '보안정책상 허용되지 않습니다.'; +$lang->msg_method_not_allowed = '이 요청에 사용할 수 없는 HTTP 메소드입니다.'; $lang->msg_feature_disabled = '사용할 수 없는 기능입니다.'; $lang->msg_error_occured = '오류가 발생했습니다.'; $lang->msg_not_founded = '대상을 찾을 수 없습니다.'; diff --git a/common/manual/server_config/rhymix-nginx-subdir.conf b/common/manual/server_config/rhymix-nginx-subdir.conf index 69b8ef0df..06d370827 100644 --- a/common/manual/server_config/rhymix-nginx-subdir.conf +++ b/common/manual/server_config/rhymix-nginx-subdir.conf @@ -25,33 +25,7 @@ location ~ ^/rhymix/(.+)\.min\.(css|js)$ { try_files $uri $uri/ /rhymix/$1.$2; } -# rss, blogAPI -rewrite ^/rhymix/(rss|atom)$ /rhymix/index.php?module=rss&act=$1 last; -rewrite ^/rhymix/([a-zA-Z0-9_]+)/(rss|atom|api)$ /rhymix/index.php?mid=$1&act=$2 last; - -# trackback -rewrite ^/rhymix/([0-9]+)/(.+)/trackback$ /rhymix/index.php?document_srl=$1&key=$2&act=trackback last; -rewrite ^/rhymix/([a-zA-Z0-9_]+)/([0-9]+)/(.+)/trackback$ /rhymix/index.php?mid=$1&document_srl=$2&key=$3&act=trackback last; - -# administrator page -rewrite ^/rhymix/admin/?$ /rhymix/index.php?module=admin last; - -# document category -rewrite ^/rhymix/([a-zA-Z0-9_]+)/category/([0-9]+)$ /rhymix/index.php?mid=$1&category=$2 last; - -# document permanent link -rewrite ^/rhymix/([0-9]+)$ /rhymix/index.php?document_srl=$1 last; - -# mid link -location ~ ^/rhymix/([a-zA-Z0-9_]+)/?$ { - try_files $uri $uri/ /rhymix/index.php?mid=$1; +# all other short URLs +location /rhymix/ { + try_files $uri $uri/ /rhymix/index.php$is_args$args; } - -# mid + document link -rewrite ^/rhymix/([a-zA-Z0-9_]+)/([0-9]+)$ /rhymix/index.php?mid=$1&document_srl=$2 last; - -# mid + entry title -rewrite ^/rhymix/([a-zA-Z0-9_]+)/entry/(.+)$ /rhymix/index.php?mid=$1&entry=$2 last; - -# file download -rewrite ^/rhymix/files/download/([0-9]+)/([a-zA-Z0-9_-]+)/(.+)$ /rhymix/index.php?act=procFileOutput&file_srl=$1&file_key=$2&filename=$3 last; diff --git a/common/manual/server_config/rhymix-nginx.conf b/common/manual/server_config/rhymix-nginx.conf index 11aed6f3f..04306e0bd 100644 --- a/common/manual/server_config/rhymix-nginx.conf +++ b/common/manual/server_config/rhymix-nginx.conf @@ -25,33 +25,7 @@ location ~ ^/(.+)\.min\.(css|js)$ { try_files $uri $uri/ /$1.$2; } -# rss, blogAPI -rewrite ^/(rss|atom)$ /index.php?module=rss&act=$1 last; -rewrite ^/([a-zA-Z0-9_]+)/(rss|atom|api)$ /index.php?mid=$1&act=$2 last; - -# trackback -rewrite ^/([0-9]+)/(.+)/trackback$ /index.php?document_srl=$1&key=$2&act=trackback last; -rewrite ^/([a-zA-Z0-9_]+)/([0-9]+)/(.+)/trackback$ /index.php?mid=$1&document_srl=$2&key=$3&act=trackback last; - -# administrator page -rewrite ^/admin/?$ /index.php?module=admin last; - -# document category -rewrite ^/([a-zA-Z0-9_]+)/category/([0-9]+)$ /index.php?mid=$1&category=$2 last; - -# document permanent link -rewrite ^/([0-9]+)$ /index.php?document_srl=$1 last; - -# mid link -location ~ ^/([a-zA-Z0-9_]+)/?$ { - try_files $uri $uri/ /index.php?mid=$1; +# all other short URLs +location / { + try_files $uri $uri/ /index.php$is_args$args; } - -# mid + document link -rewrite ^/([a-zA-Z0-9_]+)/([0-9]+)$ /index.php?mid=$1&document_srl=$2 last; - -# mid + entry title -rewrite ^/([a-zA-Z0-9_]+)/entry/(.+)$ /index.php?mid=$1&entry=$2 last; - -# file download -rewrite ^/files/download/([0-9]+)/([a-zA-Z0-9_-]+)/(.+)$ /index.php?act=procFileOutput&file_srl=$1&file_key=$2&filename=$3 last; diff --git a/modules/admin/admin.admin.controller.php b/modules/admin/admin.admin.controller.php index 7ae1e22cd..3b70b53a7 100644 --- a/modules/admin/admin.admin.controller.php +++ b/modules/admin/admin.admin.controller.php @@ -791,7 +791,8 @@ class adminAdminController extends admin Rhymix\Framework\Config::set('locale.default_timezone', $vars->default_timezone); // Other settings - Rhymix\Framework\Config::set('use_rewrite', $vars->use_rewrite === 'Y'); + Rhymix\Framework\Config::set('url.rewrite', intval($vars->use_rewrite)); + Rhymix\Framework\Config::set('use_rewrite', $vars->use_rewrite > 0); Rhymix\Framework\Config::set('session.delay', $vars->delay_session === 'Y'); Rhymix\Framework\Config::set('session.use_db', $vars->use_db_session === 'Y'); Rhymix\Framework\Config::set('view.manager_layout', $vars->manager_layout ?: 'module'); diff --git a/modules/admin/admin.admin.view.php b/modules/admin/admin.admin.view.php index bc6967233..3a2732c19 100644 --- a/modules/admin/admin.admin.view.php +++ b/modules/admin/admin.admin.view.php @@ -312,7 +312,26 @@ class adminAdminView extends admin { $needUpdate = FALSE; $addTables = FALSE; - foreach($module_list AS $key => $value) + $priority = array( + 'module' => 1000000, + 'member' => 100000, + 'document' => 10000, + 'comment' => 1000, + 'file' => 100, + ); + usort($module_list, function($a, $b) use($priority) { + $a_priority = isset($priority[$a->module]) ? $priority[$a->module] : 0; + $b_priority = isset($priority[$b->module]) ? $priority[$b->module] : 0; + if ($a_priority == 0 && $b_priority == 0) + { + return strcmp($a->module, $b->module); + } + else + { + return $b_priority - $a_priority; + } + }); + foreach($module_list as $value) { if($value->need_install) { @@ -544,7 +563,7 @@ class adminAdminView extends admin Context::set('selected_timezone', Rhymix\Framework\Config::get('locale.default_timezone')); // Other settings - Context::set('use_rewrite', Rhymix\Framework\Config::get('use_rewrite')); + Context::set('use_rewrite', Rhymix\Framework\Router::getRewriteLevel()); Context::set('use_mobile_view', (config('mobile.enabled') !== null ? config('mobile.enabled') : config('use_mobile_view')) ? true : false); Context::set('tablets_as_mobile', config('mobile.tablets') ? true : false); Context::set('mobile_viewport', config('mobile.viewport') ?: 'width=device-width, initial-scale=1.0, user-scalable=yes'); diff --git a/modules/admin/lang/en.php b/modules/admin/lang/en.php index 2649d5e7e..9a0b06451 100644 --- a/modules/admin/lang/en.php +++ b/modules/admin/lang/en.php @@ -264,7 +264,11 @@ $lang->trash = 'Recycle Bin'; $lang->accusation = 'Report'; $lang->status = 'Status'; $lang->action = 'Execute'; -$lang->use_rewrite = 'Use Rewrite Mode'; +$lang->use_rewrite = 'Use Short URLs'; +$lang->use_rewrite_0 = 'None'; +$lang->use_rewrite_1 = 'XE-compatible URLs only'; +$lang->use_rewrite_2 = 'All supported URLs'; +$lang->about_use_rewrite = 'Your web server must support mod_rewrite in order for short URLs to work. Apache usually detects the .htaccess file automatically.
nginx users should configure rewrite rules according to the manual. Outdated versions of nginx rewrite rules only support XE-compatible short URLs.'; $lang->timezone = 'Time Zone'; $lang->use_mobile_view = 'Enable Mobile View'; $lang->about_use_mobile_view = 'Show mobile page when visitors access with mobile devices.'; diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php index 7aa1e75f6..e578a05ee 100644 --- a/modules/admin/lang/ko.php +++ b/modules/admin/lang/ko.php @@ -261,6 +261,10 @@ $lang->accusation = '신고'; $lang->status = '상태'; $lang->action = '실행'; $lang->use_rewrite = '짧은 주소 사용'; +$lang->use_rewrite_0 = '사용하지 않음'; +$lang->use_rewrite_1 = 'XE와 호환되는 주소 형태만 사용'; +$lang->use_rewrite_2 = '모든 주소 형태를 사용'; +$lang->about_use_rewrite = '짧은 주소를 사용하려면 웹서버에서 rewrite 기능을 지원해야 합니다. 아파치의 경우 .htaccess 파일이 있으면 대부분 자동으로 인식합니다.
nginx 사용시 매뉴얼을 참고하여 직접 설정하셔야 합니다. 구 버전의 nginx 설정은 XE와 호환되는 주소만 지원하니 주의하십시오.'; $lang->timezone = '표준 시간대'; $lang->use_mobile_view = '모바일 뷰 사용'; $lang->about_use_mobile_view = '모바일 기기로 접속시 모바일 페이지를 보여줍니다.'; diff --git a/modules/admin/tpl/config_advanced.html b/modules/admin/tpl/config_advanced.html index df1d9125f..d68581567 100644 --- a/modules/admin/tpl/config_advanced.html +++ b/modules/admin/tpl/config_advanced.html @@ -13,8 +13,10 @@
- - + + + +

{$lang->about_use_rewrite}

diff --git a/modules/board/board.class.php b/modules/board/board.class.php index ee1e7db20..30ef25e63 100644 --- a/modules/board/board.class.php +++ b/modules/board/board.class.php @@ -25,18 +25,6 @@ class board extends ModuleObject */ function __construct() { - if(!Context::isInstalled()) return; - - if(!Context::isExistsSSLAction('dispBoardWrite') && Context::getSslStatus() == 'optional') - { - $ssl_actions = array('dispBoardWrite', 'dispBoardWriteComment', 'dispBoardReplyComment', 'dispBoardModifyComment', 'dispBoardDelete', 'dispBoardDeleteComment', 'procBoardInsertDocument', 'procBoardDeleteDocument', 'procBoardInsertComment', 'procBoardDeleteComment', 'procBoardVerificationPassword'); - Context::addSSLActions($ssl_actions); - } - if(!Context::isExistsSSLAction('dispTempSavedList') && Context::getSslStatus() == 'optional') - { - Context::addSSLAction('dispTempSavedList'); - } - parent::__construct(); } diff --git a/modules/board/board.view.php b/modules/board/board.view.php index 8ffa9c4d4..183ed018e 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -1364,6 +1364,11 @@ class boardView extends board Context::set('blame_member_info', $blame_member_infos); $this->setTemplateFile('vote_log'); } + + function dispBoardNotFound() + { + $this->alertMessage('msg_not_founded', 404); + } /** * @brief the method for displaying the warning messages diff --git a/modules/board/conf/module.xml b/modules/board/conf/module.xml index 8bd36c9cd..6b7ab3e2c 100644 --- a/modules/board/conf/module.xml +++ b/modules/board/conf/module.xml @@ -56,19 +56,43 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -79,13 +103,13 @@ - - - - - - - + + + + + + + @@ -102,7 +126,7 @@ - + diff --git a/modules/document/conf/module.xml b/modules/document/conf/module.xml index 9ae70f820..3d5221aee 100644 --- a/modules/document/conf/module.xml +++ b/modules/document/conf/module.xml @@ -4,7 +4,7 @@ - + diff --git a/modules/install/install.admin.controller.php b/modules/install/install.admin.controller.php index 931933410..5ebd34302 100644 --- a/modules/install/install.admin.controller.php +++ b/modules/install/install.admin.controller.php @@ -24,7 +24,9 @@ class installAdminController extends install $oInstallController = getController('install'); $oInstallController->installModule($module_name, './modules/'.$module_name); - + $oModuleController = getController('module'); + $oModuleController->registerActionForwardRoutes($module_name); + $oModuleController->registerSecureActions($module_name); $this->setMessage('success_installed'); } @@ -41,13 +43,29 @@ class installAdminController extends install if(!$oModule) throw new Rhymix\Framework\Exceptions\InvalidRequest; Rhymix\Framework\Session::close(); + $output = $oModule->moduleUpdate(); - Rhymix\Framework\Session::start(); if($output instanceof BaseObject && !$output->toBool()) { + Rhymix\Framework\Session::start(); return $output; } + $oModuleController = getController('module'); + $output = $oModuleController->registerActionForwardRoutes($module_name); + if($output instanceof BaseObject && !$output->toBool()) + { + Rhymix\Framework\Session::start(); + return $output; + } + $output = $oModuleController->registerSecureActions($module_name); + if($output instanceof BaseObject && !$output->toBool()) + { + Rhymix\Framework\Session::start(); + return $output; + } + + Rhymix\Framework\Session::start(); $this->setMessage('success_updated'); } diff --git a/modules/integration_search/conf/module.xml b/modules/integration_search/conf/module.xml index 89e20d10a..a3f3e86fe 100644 --- a/modules/integration_search/conf/module.xml +++ b/modules/integration_search/conf/module.xml @@ -2,7 +2,11 @@ - + + + + + diff --git a/modules/integration_search/integration_search.class.php b/modules/integration_search/integration_search.class.php index 93adb3d97..a2cbc7c34 100644 --- a/modules/integration_search/integration_search.class.php +++ b/modules/integration_search/integration_search.class.php @@ -38,6 +38,7 @@ class integration_search extends ModuleObject if(is_dir($template_path)) return true; } } + return false; } diff --git a/modules/member/conf/module.xml b/modules/member/conf/module.xml index 446984c48..9ee23c64e 100644 --- a/modules/member/conf/module.xml +++ b/modules/member/conf/module.xml @@ -2,42 +2,42 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/modules/member/member.class.php b/modules/member/member.class.php index a69fb96be..07ea21682 100644 --- a/modules/member/member.class.php +++ b/modules/member/member.class.php @@ -20,15 +20,6 @@ class member extends ModuleObject { */ function __construct() { - if(!Context::isInstalled()) return; - - // Set to use SSL upon actions related member join/information/password and so on. 2013.02.15 - if(!Context::isExistsSSLAction('dispMemberModifyPassword') && Context::getSslStatus() == 'optional') - { - $ssl_actions = array('dispMemberModifyPassword', 'dispMemberSignUpForm', 'dispMemberModifyInfo', 'dispMemberModifyEmailAddress', 'dispMemberResendAuthMail', 'dispMemberLoginForm', 'dispMemberFindAccount', 'dispMemberLeave', 'procMemberLogin', 'procMemberModifyPassword', 'procMemberInsert', 'procMemberModifyInfo', 'procMemberFindAccount', 'procMemberModifyEmailAddress', 'procMemberResendAuthMail', 'procMemberLeave'/*, 'getMemberMenu'*/, 'procMemberFindAccountByQuestion'); - Context::addSSLActions($ssl_actions); - } - parent::__construct(); } diff --git a/modules/module/module.class.php b/modules/module/module.class.php index 392db552c..a092b2e9f 100644 --- a/modules/module/module.class.php +++ b/modules/module/module.class.php @@ -131,6 +131,11 @@ class module extends ModuleObject { return true; } + + // check route columns in action_forward table + if(!$oDB->isColumnExists('action_forward', 'route_regexp')) return true; + if(!$oDB->isColumnExists('action_forward', 'route_config')) return true; + if(!$oDB->isColumnExists('action_forward', 'global_route')) return true; } /** @@ -448,6 +453,20 @@ class module extends ModuleObject $oDB->addIndex('module_part_config', 'unique_module_part_config', array('module', 'module_srl'), false); } } + + // check route columns in action_forward table + if(!$oDB->isColumnExists('action_forward', 'route_regexp')) + { + $oDB->addColumn('action_forward', 'route_regexp', 'text'); + } + if(!$oDB->isColumnExists('action_forward', 'route_config')) + { + $oDB->addColumn('action_forward', 'route_config', 'text'); + } + if(!$oDB->isColumnExists('action_forward', 'global_route')) + { + $oDB->addColumn('action_forward', 'global_route', 'char', 1, 'N', true); + } } /** diff --git a/modules/module/module.controller.php b/modules/module/module.controller.php index 1bcf0898f..90116d661 100644 --- a/modules/module/module.controller.php +++ b/modules/module/module.controller.php @@ -19,13 +19,15 @@ class moduleController extends module * Action forward finds and forwards if an action is not in the requested module * This is used when installing a module */ - function insertActionForward($module, $type, $act) + function insertActionForward($module, $type, $act, $route_regexp = null, $route_config = null, $global_route = 'N') { $args = new stdClass(); $args->module = $module; $args->type = $type; $args->act = $act; - + $args->route_regexp = is_scalar($route_regexp) ? $route_regexp : serialize($route_regexp); + $args->route_config = is_scalar($route_config) ? $route_config : serialize($route_config); + $args->global_route = $global_route === 'Y' ? 'Y' : 'N'; $output = executeQuery('module.insertActionForward', $args); Rhymix\Framework\Cache::delete('action_forward'); @@ -48,6 +50,32 @@ class moduleController extends module return $output; } + /** + * @brief Add action security + */ + function insertActionSecurity($act) + { + $args = new stdClass(); + $args->act = $act; + $output = executeQuery('module.insertActionSecurity', $args); + + Rhymix\Framework\Cache::delete('action_security'); + return $output; + } + + /** + * @brief Delete action security + */ + function deleteActionSecurity($act) + { + $args = new stdClass(); + $args->act = $act; + $output = executeQuery('module.deleteActionSecurity', $args); + + Rhymix\Framework\Cache::delete('action_security'); + return $output; + } + /** * @brief Add trigger callback function * @@ -1288,6 +1316,102 @@ class moduleController extends module Rhymix\Framework\Cache::clearGroup('site_and_module'); return $output; } + + /** + * Check if all action-forwardable routes are registered. If not, register them. + * + * @param string $module_name + * @return object + */ + public function registerActionForwardRoutes(string $module_name) + { + $action_forward = ModuleModel::getActionForward(); + $module_action_info = ModuleModel::getModuleActionXml($module_name); + + // Get the list of forwardable actions and their routes. + $forwardable_routes = array(); + foreach ($module_action_info->action ?: [] as $action_name => $action_info) + { + if (count($action_info->route) && $action_info->standalone !== 'false') + { + $forwardable_routes[$action_name] = array( + 'type' => $module_action_info->action->{$action_name}->type, + 'regexp' => array(), + 'config' => $action_info->route, + 'global_route' => $action_info->global_route === 'true' ? 'Y' : 'N', + ); + } + } + foreach ($module_action_info->route->GET as $regexp => $action_name) + { + if (isset($forwardable_routes[$action_name])) + { + $forwardable_routes[$action_name]['regexp'][] = ['GET', $regexp]; + } + } + foreach ($module_action_info->route->POST as $regexp => $action_name) + { + if (isset($forwardable_routes[$action_name])) + { + $forwardable_routes[$action_name]['regexp'][] = ['POST', $regexp]; + } + } + + // Insert or delete from the action_forward table. + foreach ($forwardable_routes as $action_name => $route_info) + { + if (!isset($action_forward[$action_name])) + { + $output = $this->insertActionForward($module_name, $route_info['type'], $action_name, + $route_info['regexp'], $route_info['config'], $route_info['global_route']); + if (!$output->toBool()) + { + return $output; + } + } + elseif ($action_forward[$action_name]->route_regexp !== $route_info['regexp'] || + $action_forward[$action_name]->route_config !== $route_info['config'] || + $action_forward[$action_name]->global_route !== $route_info['global_route']) + { + $output = $this->deleteActionForward($module_name, $route_info['type'], $action_name); + if (!$output->toBool()) + { + return $output; + } + + $output = $this->insertActionForward($module_name, $route_info['type'], $action_name, + $route_info['regexp'], $route_info['config'], $route_info['global_route']); + if (!$output->toBool()) + { + return $output; + } + } + } + + return new BaseObject(); + } + + /** + * Check if all secure actions are registered. If not, register them. + * + * @param string $module_name + * @return object + */ + public function registerSecureActions(string $module_name) + { + $action_security = ModuleModel::getActionSecurity(); + $module_action_info = ModuleModel::getModuleActionXml($module_name); + + foreach ($module_action_info->action ?: [] as $action_name => $action_info) + { + if ($action_info->use_ssl === 'true' && !isset($action_security[$action_name])) + { + $output = $this->insertActionSecurity($action_name); + } + } + + return new BaseObject(); + } } /* End of file module.controller.php */ /* Location: ./modules/module/module.controller.php */ diff --git a/modules/module/module.model.php b/modules/module/module.model.php index c7c249dd4..dc72f05db 100644 --- a/modules/module/module.model.php +++ b/modules/module/module.model.php @@ -211,7 +211,7 @@ class moduleModel extends module */ public static function getModuleInfoByMid($mid, $site_srl = 0, $columnList = array()) { - if(!$mid || ($mid && !preg_match("/^[a-z][a-z0-9_]+$/i", $mid))) + if(!$mid || ($mid && !preg_match("/^[a-z][a-z0-9_-]+$/i", $mid))) { return; } @@ -567,7 +567,7 @@ class moduleModel extends module /** * @brief Get forward value by the value of act */ - public static function getActionForward($act) + public static function getActionForward($act = null) { $action_forward = Rhymix\Framework\Cache::get('action_forward'); if($action_forward === null) @@ -582,12 +582,18 @@ class moduleModel extends module $action_forward = array(); foreach($output->data as $item) { + if ($item->route_regexp) $item->route_regexp = unserialize($item->route_regexp); + if ($item->route_config) $item->route_config = unserialize($item->route_config); $action_forward[$item->act] = $item; } Rhymix\Framework\Cache::set('action_forward', $action_forward, 0, true); } + if(!isset($act)) + { + return $action_forward; + } if(!isset($action_forward[$act])) { return; @@ -596,6 +602,38 @@ class moduleModel extends module return $action_forward[$act]; } + /** + * @brief Get SSL action setting + */ + public static function getActionSecurity($act = null) + { + $action_security = Rhymix\Framework\Cache::get('action_security'); + if($action_security === null) + { + $args = new stdClass(); + $output = executeQueryArray('module.getActionSecurity', $args); + if(!$output->toBool()) + { + return; + } + + $action_security = array(); + foreach($output->data as $item) + { + $action_security[$item->act] = true; + } + + Rhymix\Framework\Cache::set('action_security', $action_security, 0, true); + } + + if(!isset($act)) + { + return $action_security; + } + + return isset($action_security[$act]) ? true : false; + } + /** * @brief Get trigger functions */ @@ -686,321 +724,50 @@ class moduleModel extends module */ public static function getModuleInfoXml($module) { - // Get a path of the requested module. Return if not exists. + // Check the path and XML file name. $module_path = ModuleHandler::getModulePath($module); - if(!$module_path) return; - // Read the xml file for module skin information - $xml_file = sprintf("%s/conf/info.xml", $module_path); - if(!file_exists($xml_file)) return; - - $oXmlParser = new XmlParser(); - $tmp_xml_obj = $oXmlParser->loadXmlFile($xml_file); - $xml_obj = $tmp_xml_obj->module; - - if(!$xml_obj) return; - - // Module Information - $module_info = new stdClass(); - if($xml_obj->version && $xml_obj->attrs->version == '0.2') + if (!$module_path) return; + $xml_file = $module_path . 'conf/info.xml'; + if (!file_exists($xml_file)) return; + + // Load the XML file and cache the definition. + $mtime1 = filemtime($xml_file); + $mtime2 = file_exists($module_path . 'conf/module.xml') ? filemtime($module_path . 'conf/module.xml') : 0; + $cache_key = sprintf('site_and_module:module_info_xml:%s:%d:%d', $module, $mtime1, $mtime2); + $info = Rhymix\Framework\Cache::get($cache_key); + if($info === null) { - // module format 0.2 - $module_info->title = $xml_obj->title->body; - $module_info->description = $xml_obj->description->body; - $module_info->version = $xml_obj->version->body; - $module_info->homepage = $xml_obj->link->body; - $module_info->category = $xml_obj->category->body; - if(!$module_info->category) $module_info->category = 'service'; - $date_obj = (object)array('y' => 0, 'm' => 0, 'd' => 0); - sscanf($xml_obj->date->body, '%d-%d-%d', $date_obj->y, $date_obj->m, $date_obj->d); - $module_info->date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d); - $module_info->license = $xml_obj->license->body; - $module_info->license_link = $xml_obj->license->attrs->link; - - if(!is_array($xml_obj->author)) $author_list[] = $xml_obj->author; - else $author_list = $xml_obj->author; - - foreach($author_list as $author) - { - $author_obj = new stdClass(); - $author_obj->name = $author->name->body; - $author_obj->email_address = $author->attrs->email_address; - $author_obj->homepage = $author->attrs->link; - $module_info->author[] = $author_obj; - } + $info = Rhymix\Framework\Parsers\ModuleInfoParser::loadXML($xml_file); + Rhymix\Framework\Cache::set($cache_key, $info, 0, true); } - else - { - // module format 0.1 - $module_info->title = $xml_obj->title->body; - $module_info->description = $xml_obj->author->description->body; - $module_info->version = $xml_obj->attrs->version; - $module_info->category = $xml_obj->attrs->category; - if(!$module_info->category) $module_info->category = 'service'; - $date_obj = (object)array('y' => 0, 'm' => 0, 'd' => 0); - sscanf($xml_obj->author->attrs->date, '%d. %d. %d', $date_obj->y, $date_obj->m, $date_obj->d); - $module_info->date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d); - $author_obj = new stdClass(); - $author_obj->name = $xml_obj->author->name->body; - $author_obj->email_address = $xml_obj->author->attrs->email_address; - $author_obj->homepage = $xml_obj->author->attrs->link; - $module_info->author[] = $author_obj; - } - // Add admin_index by using action information - $action_info = self::getModuleActionXml($module); - $module_info->admin_index_act = $action_info->admin_index_act; - $module_info->default_index_act = $action_info->default_index_act; - $module_info->setup_index_act = $action_info->setup_index_act; - $module_info->simple_setup_index_act = $action_info->simple_setup_index_act; - - return $module_info; + + return $info; } /** - * @brief Return permisson and action data by conf/module.xml in the module - * Cache it because it takes too long to parse module.xml file - * When caching, add codes so to include it directly - * This is apparently good for performance, but not sure about its side-effects + * @brief Return permisson and action data by conf/module.xml */ public static function getModuleActionXml($module) { - // Get a path of the requested module. Return if not exists. - $class_path = ModuleHandler::getModulePath($module); - if(!$class_path) return; - - // Check if module.xml exists in the path. Return if not exist - $xml_file = sprintf("%sconf/module.xml", $class_path); - if(!file_exists($xml_file)) return; - - // Check if cached file exists - $cache_file = sprintf(_XE_PATH_ . "files/cache/module_info/%s.%s.%s.php", $module, Context::getLangType(), __XE_VERSION__); - - // Update if no cache file exists or it is older than xml file - if(!file_exists($cache_file) || filemtime($cache_file) < filemtime($xml_file) || $re_cache) + // Check the path and XML file name. + $module_path = ModuleHandler::getModulePath($module); + if (!$module_path) return; + $xml_file = $module_path . 'conf/module.xml'; + if (!file_exists($xml_file)) return; + + // Load the XML file and cache the definition. + $mtime = filemtime($xml_file); + $cache_key = sprintf('site_and_module:module_action_xml:%s:%d', $module, $mtime); + $info = Rhymix\Framework\Cache::get($cache_key); + if($info === null) { - $info = new stdClass(); - $buff = array(); // /< Set buff variable to use in the cache file - $buff[] = 'default_index_act = \'%s\';'; - $buff['setup_index_act'] = '$info->setup_index_act=\'%s\';'; - $buff['simple_setup_index_act'] = '$info->simple_setup_index_act=\'%s\';'; - $buff['admin_index_act'] = '$info->admin_index_act = \'%s\';'; - - $xml_obj = XmlParser::loadXmlFile($xml_file); // /< Read xml file and convert it to xml object - - if(!countobj($xml_obj->module)) return; // /< Error occurs if module tag doesn't included in the xml - - $grants = $xml_obj->module->grants->grant; // /< Permission information - $permissions = $xml_obj->module->permissions->permission; // /< Acting permission - $menus = $xml_obj->module->menus->menu; - $actions = $xml_obj->module->actions->action; // /< Action list (required) - - $default_index = $admin_index = ''; - - // Arrange permission information - if($grants) - { - if(is_array($grants)) $grant_list = $grants; - else $grant_list[] = $grants; - - $info->grant = new stdClass(); - $buff[] = '$info->grant = new stdClass;'; - foreach($grant_list as $grant) - { - $name = $grant->attrs->name; - $default = $grant->attrs->default?$grant->attrs->default:'guest'; - $title = $grant->title->body; - - $info->grant->{$name} = new stdClass(); - $info->grant->{$name}->title = $title; - $info->grant->{$name}->default = $default; - - $buff[] = sprintf('$info->grant->%s = new stdClass;', $name); - $buff[] = sprintf('$info->grant->%s->title=\'%s\';', $name, $title); - $buff[] = sprintf('$info->grant->%s->default=\'%s\';', $name, $default); - } - } - // Permissions to grant - if($permissions) - { - if(is_array($permissions)) $permission_list = $permissions; - else $permission_list[] = $permissions; - - $buff[] = '$info->permission = new stdClass;'; - $buff[] = '$info->permission_check = new stdClass;'; - - $info->permission = new stdClass; - $info->permission_check = new stdClass; - - foreach($permission_list as $permission) - { - $action = $permission->attrs->action; - $target = $permission->attrs->target; - - $info->permission->$action = $target; - - $buff[] = sprintf('$info->permission->%s = \'%s\';', $action, $target); - - $info->permission_check->$action = new stdClass; - $info->permission_check->$action->key = $permission->attrs->check_var ?: ''; - $info->permission_check->$action->type = $permission->attrs->check_type ?: ''; - - $buff[] = sprintf('$info->permission_check->%s = new stdClass;', $action); - $buff[] = sprintf('$info->permission_check->%s->key = \'%s\';', $action, $info->permission_check->$action->key); - $buff[] = sprintf('$info->permission_check->%s->type = \'%s\';', $action, $info->permission_check->$action->type); - } - } - // for admin menus - if($menus) - { - if(is_array($menus)) $menu_list = $menus; - else $menu_list[] = $menus; - - $buff[] = '$info->menu = new stdClass;'; - $info->menu = new stdClass(); - - foreach($menu_list as $menu) - { - $menu_name = $menu->attrs->name; - $menu_title = is_array($menu->title) ? $menu->title[0]->body : $menu->title->body; - $menu_type = $menu->attrs->type; - - $info->menu->{$menu_name} = new stdClass(); - $info->menu->{$menu_name}->title = $menu_title; - $info->menu->{$menu_name}->acts = array(); - $info->menu->{$menu_name}->type = $menu_type; - - $buff[] = sprintf('$info->menu->%s = new stdClass;', $menu_name); - $buff[] = sprintf('$info->menu->%s->title=\'%s\';', $menu_name, $menu_title); - $buff[] = sprintf('$info->menu->%s->type=\'%s\';', $menu_name, $menu_type); - } - } - - // actions - if($actions) - { - if(is_array($actions)) $action_list = $actions; - else $action_list[] = $actions; - - if(!isset($info->permission)) - { - $buff[] = '$info->permission = new stdClass;'; - $buff[] = '$info->permission_check = new stdClass;'; - - $info->permission = new stdClass; - $info->permission_check = new stdClass; - } - - $buff[] = '$info->action = new stdClass;'; - $info->action = new stdClass(); - - foreach($action_list as $action) - { - $name = $action->attrs->name; - - // - if($action->attrs->permission) - { - $info->permission->$name = $action->attrs->permission; - - $buff[] = sprintf('$info->permission->%s = \'%s\';', $name, $info->permission->$name); - - $info->permission_check->$name = new stdClass; - $info->permission_check->$name->key = $action->attrs->check_var ?: ''; - $info->permission_check->$name->type = $action->attrs->check_type ?: ''; - - $buff[] = sprintf('$info->permission_check->%s = new stdClass;', $name); - $buff[] = sprintf('$info->permission_check->%s->key = \'%s\';', $name, $info->permission_check->$name->key); - $buff[] = sprintf('$info->permission_check->%s->type = \'%s\';', $name, $info->permission_check->$name->type); - } - - $type = $action->attrs->type; - $grant = $action->attrs->grant?$action->attrs->grant:'guest'; - $standalone = $action->attrs->standalone=='false'?'false':'true'; - $ruleset = $action->attrs->ruleset?$action->attrs->ruleset:''; - $method = $action->attrs->method?$action->attrs->method:''; - $check_csrf = $action->attrs->check_csrf=='false'?'false':'true'; - $meta_noindex = $action->attrs->{'meta-noindex'} === 'true' ? 'true' : 'false'; - - $index = $action->attrs->index; - $admin_index = $action->attrs->admin_index; - $setup_index = $action->attrs->setup_index; - $simple_setup_index = $action->attrs->simple_setup_index; - $menu_index = $action->attrs->menu_index; - - $info->action->{$name} = new stdClass(); - $info->action->{$name}->type = $type; - $info->action->{$name}->grant = $grant; - $info->action->{$name}->standalone = $standalone; - $info->action->{$name}->ruleset = $ruleset; - $info->action->{$name}->method = $method; - $info->action->{$name}->check_csrf = $check_csrf; - $info->action->{$name}->meta_noindex = $meta_noindex; - if($action->attrs->menu_name) - { - $info->menu->{$action->attrs->menu_name} = new stdClass(); - if($menu_index == 'true') - { - $info->menu->{$action->attrs->menu_name}->index = $name; - $buff[] = sprintf('$info->menu->%s->index=\'%s\';', $action->attrs->menu_name, $name); - } - if(is_array($info->menu->{$action->attrs->menu_name}->acts)) - { - $info->menu->{$action->attrs->menu_name}->acts[] = $name; - } - - $buff[] = sprintf('$info->menu->%s->acts[]=\'%s\';', $action->attrs->menu_name, $name); - $i++; - } - - $buff[] = sprintf('$info->action->%s = new stdClass;', $name); - $buff[] = sprintf('$info->action->%s->type=\'%s\';', $name, $type); - $buff[] = sprintf('$info->action->%s->grant=\'%s\';', $name, $grant); - $buff[] = sprintf('$info->action->%s->standalone=\'%s\';', $name, $standalone); - $buff[] = sprintf('$info->action->%s->ruleset=\'%s\';', $name, $ruleset); - $buff[] = sprintf('$info->action->%s->method=\'%s\';', $name, $method); - $buff[] = sprintf('$info->action->%s->check_csrf=\'%s\';', $name, $check_csrf); - $buff[] = sprintf('$info->action->%s->meta_noindex=\'%s\';', $name, $meta_noindex); - - if($index=='true') - { - $default_index_act = $name; - $info->default_index_act = $name; - } - if($admin_index=='true') - { - $admin_index_act = $name; - $info->admin_index_act = $name; - } - if($setup_index=='true') - { - $setup_index_act = $name; - $info->setup_index_act = $name; - } - if($simple_setup_index=='true') - { - $simple_setup_index_act = $name; - $info->simple_setup_index_act = $name; - } - } - } - $buff['default_index_act'] = sprintf($buff['default_index_act'], $default_index_act); - $buff['setup_index_act'] = sprintf($buff['setup_index_act'], $setup_index_act); - $buff['simple_setup_index_act'] = sprintf($buff['simple_setup_index_act'], $simple_setup_index_act); - $buff['admin_index_act'] = sprintf($buff['admin_index_act'], $admin_index_act); - - $buff[] = 'return $info;'; - - $buff = implode(PHP_EOL, $buff); - - FileHandler::writeFile($cache_file, $buff); - - return $info; + $info = Rhymix\Framework\Parsers\ModuleActionParser::loadXML($xml_file); + Rhymix\Framework\Cache::set($cache_key, $info, 0, true); } - - if(file_exists($cache_file)) return include($cache_file); + + return $info; } - + /** * Get a skin list for js API. * return void @@ -1609,12 +1376,15 @@ class moduleModel extends module $searched_count = count($searched_list); if(!$searched_count) return; + + // Get action forward + $action_forward = self::getActionForward(); + + // Get action security + $action_security = self::getActionSecurity(); - for($i=0;$i<$searched_count;$i++) + foreach ($searched_list as $module_name) { - // module name - $module_name = $searched_list[$i]; - $path = ModuleHandler::getModulePath($module_name); if(!is_dir(FileHandler::getRealPath($path))) continue; @@ -1651,16 +1421,59 @@ class moduleModel extends module { $info->need_install = false; } + // Check if it is upgraded to module.class.php on each module - $oDummy = null; $oDummy = getModule($module_name, 'class'); if($oDummy && method_exists($oDummy, "checkUpdate")) { $info->need_update = $oDummy->checkUpdate(); } - else + unset($oDummy); + + // Check if all action-forwardable routes are registered + $module_action_info = self::getModuleActionXml($module_name); + $forwardable_routes = array(); + foreach ($module_action_info->action ?: [] as $action_name => $action_info) { - continue; + if (count($action_info->route) && $action_info->standalone !== 'false') + { + $forwardable_routes[$action_name] = array( + 'regexp' => array(), + 'config' => $action_info->route, + ); + } + } + foreach ($module_action_info->route->GET ?: [] as $regexp => $action_name) + { + if (isset($forwardable_routes[$action_name])) + { + $forwardable_routes[$action_name]['regexp'][] = ['GET', $regexp]; + } + } + foreach ($module_action_info->route->POST ?: [] as $regexp => $action_name) + { + if (isset($forwardable_routes[$action_name])) + { + $forwardable_routes[$action_name]['regexp'][] = ['POST', $regexp]; + } + } + foreach ($forwardable_routes as $action_name => $route_info) + { + if (!isset($action_forward[$action_name]) || + $action_forward[$action_name]->route_regexp !== $route_info['regexp'] || + $action_forward[$action_name]->route_config !== $route_info['config']) + { + $info->need_update = true; + } + } + + // Check if all secure actions are registered + foreach ($module_action_info->action ?: [] as $action_name => $action_info) + { + if ($action_info->use_ssl === 'true' && !isset($action_security[$action_name])) + { + $info->need_update = true; + } } } $list[] = $info; diff --git a/modules/module/queries/deleteActionSecurity.xml b/modules/module/queries/deleteActionSecurity.xml new file mode 100644 index 000000000..bbf0e9b1e --- /dev/null +++ b/modules/module/queries/deleteActionSecurity.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/modules/module/queries/getActionSecurity.xml b/modules/module/queries/getActionSecurity.xml new file mode 100644 index 000000000..2cdd7eed3 --- /dev/null +++ b/modules/module/queries/getActionSecurity.xml @@ -0,0 +1,11 @@ + + +
+ + + + + + + + diff --git a/modules/module/queries/insertActionForward.xml b/modules/module/queries/insertActionForward.xml index ef48a8e2b..acd9c532a 100644 --- a/modules/module/queries/insertActionForward.xml +++ b/modules/module/queries/insertActionForward.xml @@ -6,5 +6,8 @@ + + + diff --git a/modules/module/queries/insertActionSecurity.xml b/modules/module/queries/insertActionSecurity.xml new file mode 100644 index 000000000..d1cf772be --- /dev/null +++ b/modules/module/queries/insertActionSecurity.xml @@ -0,0 +1,8 @@ + + +
+ + + + + diff --git a/modules/module/schemas/action_forward.xml b/modules/module/schemas/action_forward.xml index 608bae8c8..59ad98fa8 100644 --- a/modules/module/schemas/action_forward.xml +++ b/modules/module/schemas/action_forward.xml @@ -1,5 +1,8 @@
- + + + +
diff --git a/modules/module/schemas/action_security.xml b/modules/module/schemas/action_security.xml new file mode 100644 index 000000000..5d8bb34fa --- /dev/null +++ b/modules/module/schemas/action_security.xml @@ -0,0 +1,3 @@ + + +
diff --git a/tests/unit/classes/ContextTest.php b/tests/unit/classes/ContextTest.php index 6b798ca6e..e304499d7 100644 --- a/tests/unit/classes/ContextTest.php +++ b/tests/unit/classes/ContextTest.php @@ -96,7 +96,7 @@ class ContextTest extends \Codeception\TestCase\Test Context::setRequestArguments(); $this->assertEquals('POST', Context::getRequestMethod()); $this->assertNull(Context::getRequestVars()->foo); - $this->assertEquals('bazz', Context::get('foo')); // XE Compatibility behavior + $this->assertNull(Context::get('foo')); // This is different from XE behavior $_SERVER['REQUEST_METHOD'] = 'POST'; $_GET = array(); diff --git a/tests/unit/framework/RouterTest.php b/tests/unit/framework/RouterTest.php new file mode 100644 index 000000000..a31cf0311 --- /dev/null +++ b/tests/unit/framework/RouterTest.php @@ -0,0 +1,100 @@ +assertEquals(0, Rhymix\Framework\Router::getRewriteLevel()); + + Rhymix\Framework\Config::set('url.rewrite', 1); + Rhymix\Framework\Config::set('use_rewrite', false); + $this->assertEquals(1, Rhymix\Framework\Router::getRewriteLevel()); + + Rhymix\Framework\Config::set('url.rewrite', 1); + Rhymix\Framework\Config::set('use_rewrite', true); + $this->assertEquals(1, Rhymix\Framework\Router::getRewriteLevel()); + + Rhymix\Framework\Config::set('url.rewrite', 2); + Rhymix\Framework\Config::set('use_rewrite', true); + $this->assertEquals(2, Rhymix\Framework\Router::getRewriteLevel()); + } + + public function testGetURL() + { + $info = Rhymix\Framework\Parsers\ModuleActionParser::loadXML(\RX_BASEDIR . 'modules/board/conf/module.xml'); + $this->assertEquals('dispBoardContent', $info->default_index_act); + $this->assertFalse(isset($info->permission)); + $this->assertGreaterThan(0, count($info->route->GET)); + $this->assertGreaterThan(0, count($info->action->dispBoardContent->route)); + + getController('module')->registerActionForwardRoutes('member'); + + $args = array('mid' => 'board', 'document_srl' => 123); + $this->assertEquals('board/123', Rhymix\Framework\Router::getURL($args, 2)); + $this->assertEquals('board/123', Rhymix\Framework\Router::getURL($args, 1)); + $this->assertEquals('index.php?mid=board&document_srl=123', Rhymix\Framework\Router::getURL($args, 0)); + + $args = array('mid' => 'board', 'act' => 'dispBoardWrite', 'document_srl' => 123); + $this->assertEquals('board/123/edit', Rhymix\Framework\Router::getURL($args, 2)); + $this->assertEquals('index.php?mid=board&act=dispBoardWrite&document_srl=123', Rhymix\Framework\Router::getURL($args, 1)); + $this->assertEquals('index.php?mid=board&act=dispBoardWrite&document_srl=123', Rhymix\Framework\Router::getURL($args, 0)); + + $args = array('mid' => 'board', 'act' => 'dispBoardWrite'); + $this->assertEquals('board/write', Rhymix\Framework\Router::getURL($args, 2)); + $this->assertEquals('index.php?mid=board&act=dispBoardWrite', Rhymix\Framework\Router::getURL($args, 1)); + $this->assertEquals('index.php?mid=board&act=dispBoardWrite', Rhymix\Framework\Router::getURL($args, 0)); + + $args = array('mid' => 'board', 'act' => 'dispBoardModifyComment', 'document_srl' => 123, 'comment_srl' => 456, 'extra_param' => 'foo bar'); + $this->assertEquals('board/comment/456/edit?extra_param=foo+bar', Rhymix\Framework\Router::getURL($args, 2)); + $this->assertEquals('index.php?mid=board&act=dispBoardModifyComment&document_srl=123&comment_srl=456&extra_param=foo+bar', Rhymix\Framework\Router::getURL($args, 1)); + $this->assertEquals('index.php?mid=board&act=dispBoardModifyComment&document_srl=123&comment_srl=456&extra_param=foo+bar', Rhymix\Framework\Router::getURL($args, 0)); + + $args = array('mid' => 'board', 'act' => 'dispMemberInfo'); + $this->assertEquals('board/member_info', Rhymix\Framework\Router::getURL($args, 2)); + $this->assertEquals('index.php?mid=board&act=dispMemberInfo', Rhymix\Framework\Router::getURL($args, 1)); + $this->assertEquals('index.php?mid=board&act=dispMemberInfo', Rhymix\Framework\Router::getURL($args, 0)); + + $args = array('module' => 'document', 'act' => 'procDocumentVoteUp'); + $this->assertEquals('document/procDocumentVoteUp', Rhymix\Framework\Router::getURL($args, 2)); + $this->assertEquals('index.php?module=document&act=procDocumentVoteUp', Rhymix\Framework\Router::getURL($args, 1)); + $this->assertEquals('index.php?module=document&act=procDocumentVoteUp', Rhymix\Framework\Router::getURL($args, 0)); + } + + public function testParseURL() + { + $args = array('mid' => 'board', 'act' => 'dispBoardContent', 'document_srl' => '123'); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'board/123', 2)->args); + $this->assertEquals('board/123', Rhymix\Framework\Router::parseURL('GET', 'board/123', 2)->url); + $this->assertEquals('board', Rhymix\Framework\Router::parseURL('GET', 'board/123', 2)->mid); + $this->assertEquals('dispBoardContent', Rhymix\Framework\Router::parseURL('GET', 'board/123', 2)->act); + + $args = array('mid' => 'board', 'document_srl' => '123'); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'board/123', 1)->args); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'index.php?mid=board&document_srl=123', 0)->args); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', \RX_BASEURL . 'board/123', 1)->args); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', \RX_BASEURL . 'index.php?mid=board&document_srl=123', 0)->args); + + $args = array('mid' => 'board', 'act' => 'dispBoardModifyComment', 'comment_srl' => '456', 'extra_param' => 'foo bar'); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'board/comment/456/edit?extra_param=foo+bar', 2)->args); + $this->assertEquals('board', Rhymix\Framework\Router::parseURL('GET', 'board/comment/456/edit?extra_param=foo+bar', 2)->mid); + $this->assertEquals('dispBoardModifyComment', Rhymix\Framework\Router::parseURL('GET', 'board/comment/456/edit?extra_param=foo+bar', 2)->act); + $this->assertEquals('', Rhymix\Framework\Router::parseURL('GET', 'board/comment/456/edit?extra_param=foo+bar', 2)->document_srl); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'index.php?mid=board&act=dispBoardModifyComment&comment_srl=456&extra_param=foo+bar', 1)->args); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'index.php?mid=board&act=dispBoardModifyComment&comment_srl=456&extra_param=foo+bar', 0)->args); + + $args = array('mid' => 'board', 'act' => 'dispMemberInfo'); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'board/member_info', 2)->args); + $this->assertEquals('board', Rhymix\Framework\Router::parseURL('GET', 'board/member_info', 2)->mid); + + $args = array('mid' => 'board', 'act' => 'dispMemberLoginForm'); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('GET', 'board/login', 2)->args); + $this->assertEquals('member', Rhymix\Framework\Router::parseURL('GET', 'board/login', 2)->module); + + $args = array('mid' => 'board', 'act' => 'procMemberLogin'); + $this->assertEquals($args, Rhymix\Framework\Router::parseURL('POST', 'board/login', 2)->args); + $this->assertEquals('member', Rhymix\Framework\Router::parseURL('POST', 'board/login', 2)->module); + + } +}