'[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_file($filename); if ($xml === false) { return false; } // Get the current language. $lang = \Context::getLangType(); // 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->menu = new \stdClass; $info->grant = new \stdClass; $info->permission = new \stdClass; $info->permission_check = new \stdClass; // 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 permissions not defined in the section. foreach ($xml->permissions->permission ?: [] as $permission) { $action_name = trim($permission['action']); $permission = trim($permission['target']); $info->permission->{$action_name} = $permission; $check = new \stdClass; $check->key = trim($permission['check_var']) ?: trim($permission['check-var']); $check->type = trim($permission['check_type']) ?: trim($permission['check-type']); $info->permission_check->{$action_name} = $check; } // 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']); if ($permission) { $info->permission->{$action_name} = $permission; if (isset($info->permission_check->{$action_name})) { $info->permission_check->{$action_name} = new \stdClass; } $info->permission_check->{$action_name}->key = trim($action['check_var']) ?: trim($action['check-var']); $info->permission_check->{$action_name}->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->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'; $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; } } // 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 ''; } }