array( 'constraint' => $regexp ?: null, 'value' => $value ?: null, // Taken from request url or set by "default" 'default' => $default ?: null, 'is_optional'=> boolean, 'preceded_by'=> . | / | null // This will help when building the route, only for optional vars ) */ private $_vars = array(); private $_optional_groups; # variables in the order they're defined in the route. private $_vars_names = array(); /** * Variable values taken from the request url. */ private $_vars_values = array(); /** * $var_name => $regexp */ private $_constraints = array(); private $_as; private $_subdomain; private $_paths = []; // private $_namespaces = []; private $_modules = []; private $_controller = ''; private $_action = ''; /** * Following props serve for Build */ private $_optional_parts = array(); /** * Needed to do a diff after building a route, * to see which vars were actually used. */ private $_used_vars = array(); private $_build_url; private $_build_params; private $_rails_panel = false, $_assets_route = false; public function __construct($url, $to, array $params = array()) { foreach ($params as $name => $val) { $prop = '_' . $name; if (!property_exists($this, $prop)) throw new Exception\RuntimeException( sprintf("Tried to set invalid property '%s'.", $name) ); $this->$prop = $val; } if (!$to && (is_bool(strpos($url, ':controller')) || is_bool(strpos($url, ':action')))) { $parts = explode('/', $url); $parts = array_slice($parts, 0, 2); if (!isset($parts[1])) $parts[1] = 'index'; $parts[1] = preg_split('/\W/', $parts[1], 2)[0]; $this->_to = implode('#', $parts); } else $this->_to = $to; # Set module. if ($this->_modules && $this->_to) { $this->_to = implode(UrlToken::MODULE_SEPARATOR, $this->_modules) . UrlToken::MODULE_SEPARATOR . $this->_to; } if ($this->paths) { $scope_paths = $this->paths; $scope_paths = $scope_paths ? implode('/', $scope_paths) : ''; $url && $url = '/' . $url; $this->_url = $scope_paths . $url; } else { $this->_url = $url; } if (!$this->_via) { throw new Exception\RuntimeException( sprintf( "The 'via' option can't be empty (to=>%s)", $this->_to ) ); } !is_array($this->_via) && $this->_via = array($this->_via); } public function __get($prop) { $prop_prop = '_' . $prop; if (!property_exists($this, $prop_prop)) throw new Exception\RuntimeException( sprintf("Property '%s' doesn't exist.", $prop) ); return $this->$prop_prop; } public function build() { if (!$this->_escaped_url) { $this->_check_format_var(); $this->_extract_vars(); $this->_escape_url(); $this->_set_default_vars_values(); $this->_set_alias(); } } public function match($url, $via) { if ($base_path = Rails::application()->router()->basePath()) $url = substr($url, strlen($base_path)); $this->build(); if ($this->_is_root()) return $this->_match_root($url); elseif ($this->rails_admin() && !Rails::application()->validateSafeIps()) return false; if (!in_array(strtolower($via), $this->_via)) return false; $url = ltrim($url, '/'); $regex = '/^' . $this->_escaped_url . '$/'; if (!preg_match($regex, $url, $m)) return false; array_shift($m); $var_names = array_keys($this->_vars); /** * Add variable values to _var_values and also check if any * of the matches values is also a property (like :format) * to set it. */ foreach ($m as $k => $value) { if (isset($this->_vars[$var_names[$k]]['constraint'])) { if (substr($this->_vars[$var_names[$k]]['constraint'], 0, 1) == '/') { if (!preg_match($this->_vars[$var_names[$k]]['constraint'], $value)) { return false; } } else { if ($value !== (string)$this->_vars[$var_names[$k]]['constraint']) return false; } } # Workaround for action: when "/post/", action will be "" and # will cause errors; set it to "index". if ($var_names[$k] == 'action') { $value === "" && $value = 'index'; $this->_action = $value; } elseif ($var_names[$k] == 'format') { $value === '' && $value = 'html'; $this->_format = $value; } $this->_vars[$var_names[$k]]['value'] = $value; } if (!$this->_assets_route) { if (!$this->_parse_token()) return false; // elseif (!$this->_path_exists()) { // throw new \Rails_ActionDispatch_Router_Route_Exception_ControllerFileNotFound(); // throw new Exception\Rails_ActionDispatch_Router_Route_Exception_ControllerFileNotFound(); // } } // vpe($this); return true; } public function match_with_token($token, array &$params) { if ($token === $this->_to || $token === $this->alias()) { if ($url = $this->build_url($params, false)) { return $url; } } elseif ($this->_has_variable_to()) { $to = '/' . preg_replace('/:\w+/', '(\w+)', $this->_to) . '/'; // $to = '/' . preg_replace('/:\w+/', '(\w+)', preg_quote($this->_to)) . '/'; $type = $this->_parse_variable_to(); // try { if (preg_match($to, $token, $m)) { $params[$type] = $m[1]; if ($url = $this->build_url($params, false)) { return $url; } } // } catch (\Exception $e) { // vpe($this); // } } elseif (!$this->to) { list($params['controller'], $params['action']) = explode('#', $token); if ($url = $this->build_url($params, false)) { return $url; } } return ''; } /** * Builds the URL with params passed and returns it. * The params must include all variables defined for * the route, and also must pass the regex validation, * else an exception will be thrown. * Any param that is not used by the route will be ignored. * * @return string the bare built url, with NO base_path. */ public function build_url(array $params, $raise_e = true) { // if ($this->_to == 'help#:action') $this->build(); $this->_build_params = $params; $this->_build_url = $this->_url; # If main variables aren't present, return. if (!$this->_check_main_vars()) { $this->_build_params = array(); $this->_build_url = ''; return false; } if (($groups = $this->_optional_groups) && (array_shift($groups) ?: true) && $groups) { foreach ($groups as $group) { $parsed_group = $this->_check_optional_groups($group); $this->_build_url = str_replace($group['part'], $parsed_group, $this->_build_url); } } # Remove remaining parentheses. $this->_build_url = str_replace(array('(', ')'), '', $this->_build_url); # Set build_params as its keys for later call of remaining_params() $this->_build_params = array_keys($this->_build_params); # Add leading slash as routes don't include it. return '/' . $this->_build_url; } public function build_url_path(array $params) { $build_params = []; // if (isset($params[0]) && $params[0] instanceof \Rails\ActiveRecord\Base) { // $model = array_shift($params); // } else // $model = false; $i = 0; // vpe($params); // $params = array_values($params); foreach ($this->_vars as $name => $data) { // if ($model) { // $build_params[$name] = $model->$name; // } else { // vpe($params, !isset($params[$i]), empty($data['type'])); if (!isset($params[$name]) || empty($data['type'])) continue; // vpe(is_array($params[$name])); if (is_array($params[$name])) { $build_params = array_merge($build_params, $params[$i]); } else { $build_params[$name] = $params[$name]; } // } $i++; } // vpe($build_params, $this->_vars); // vpe($this); $url = $this->build_url($build_params); // vpe // if (!$url) vpe($params, $build_params); if ($this->_build_params) { if ($query_params = array_filter(array_intersect_key($build_params, array_fill_keys($this->_build_params, true)))) $url .= '?' . http_build_query($query_params); } return $url; } public function vars() { $vars = array(); foreach ($this->_vars as $name => $params) { $vars[$name] = $params['value']; } return $vars; } public function remaining_params() { return array_fill_keys($this->_build_params, null); } public function alias() { return $this->_as; } public function to() { return $this->_to; } public function url() { return $this->_url; } public function via() { return $this->_via; } public function namespaces() { return $this->_modules; } public function modules() { return $this->_modules; } public function controller() { return $this->_controller; } public function action() { return $this->_action; } public function rails_admin() { return $this instanceof PanelRoute; // return $this->_rails_admin; } public function isPanelRoute() { return $this instanceof PanelRoute; } # TODO: Deprecated public function panel_route() { return $this->isPanelRoute(); } public function assets_route() { return $this->_assets_route; } /** * Builds the internal view path for the route out of * module, controller and action. * i.e. route for /post/show/15 would return * this path: post/show[.$format] * * * TODO: add path_names support. */ public function path(array $params = array()) { $this->build(); $path = []; if ($this->_modules) $path = $this->_modules; $path[] = $this->_controller; $path[] = isset($params['action']) ? $params['action'] : $this->_action; $format = isset($params['format']) ? '.' . $params['format'] : ''; return implode('/', $path) . $format; } public function namespace_path() { if ($this->_modules) { return implode('/', array_map(function($x) { return Rails::services()->get('inflector')->camelize($x); }, $this->_modules)); } } public function controller_file() { $path = Rails::config()->paths->controllers . '/' . (($namespacedPath = $this->namespace_path()) ? $namespacedPath . '/' : '') . Rails::services()->get('inflector')->camelize($this->controller) . 'Controller.php'; return $path; } /** * Each namespace can have its own application controller. * This is called by Rails_Application when checking for the file for ApplicationController; * If we're in a namespace, the path to the application_controller file changes. */ public function namespaced_controller_file() { if (!$this->_modules) return false; return Rails::config()->paths->controllers . '/' . $this->namespace_path() . '/application_controller.php'; } private function _check_main_vars() { if (($group = $this->_optional_groups) && ($group = array_shift($group))) { foreach ($group as $mv) { if (!isset($this->_build_params[$mv])) { return false; } else { $this->_build_url = str_replace($this->_vars[$mv]['type'].$mv, $this->_build_params[$mv], $this->_build_url); unset($this->_build_params[$mv]); } } } return true; } private function _check_optional_groups($group) { # Used for workaround. $action = false; $group_str = $part = $group['part']; unset($group['part']); $main_vars = array_shift($group); if (!$main_vars) $group_str = ''; foreach ($main_vars as $mv) { if (!isset($this->_vars[$mv])) continue; if (!isset($this->_build_params[$mv])) { $group_str = ''; break; } else { # For workaround if ($mv == 'action') $action = $this->_build_params['action']; $group_str = str_replace($this->_vars[$mv]['type'].$mv, $this->_build_params[$mv], $group_str); unset($this->_build_params[$mv]); } } if ($group_str) { foreach ($group as $subgroup) { $subpart = $subgroup['part']; $subrpl = $this->_check_optional_groups($subgroup); # For workaround if (!$subrpl) array_shift($group); $group_str = str_replace($subpart, $subrpl, $group_str); } } /** * Here comes the workaround. If the group has only one var * and it's "action" and its value is "index" and there are no * group memebers left (i.e., this current group is just something with * 'index', remove it). */ if (count($main_vars) == 1 && $main_vars[0] == 'action' && $action == 'index' && !$group) { $group_str = ''; } return $group_str; } private function _match_root($url) { if ($url === $this->_root_url()) { $this->_parse_token(); return true; } return false; } /** * Doing this because of the options for the * :format var, such as setting it to true or false. * By default, it's always added by the end of the * route as an optional var. */ private function _check_format_var() { if ($this->_format !== false && !$this->_is_root() && !preg_match('/(:|\*)format[^\w]?/', $this->_url)) { if ($this->_format === true) $this->_url .= '.:format'; else $this->_url .= '(.:format)'; } } private function _escape_url() { $this->_escaped_url = str_replace(array( '(', ')', '.', '/', ), array( '(?:', ')?', '\.', '\/' ), $this->_url); $repls = $subjs = array(); foreach ($this->_vars as $name => $var) { if ($name == 'format') $repl = '([a-zA-Z0-9]{1,5})'; elseif ($var['type'] == '*') $repl = '(.*?)'; else $repl = '([^\/]+?)'; $repls[] = $repl; $type = $var['type'] == '*' ? '\*' : $var['type']; $subjs[] = '/' . $type . $name . '/'; } $this->_escaped_url = preg_replace($subjs, $repls, $this->_escaped_url); if (!$this->_assets_route) $this->_escaped_url .= '\/?'; } private function _find_vars($url_part = null) { $vars = array(); if ($url_part === null) { $url_part = $this->_url; } else { $vars['part'] = $url_part; # Remove parentheses. $url_part = substr($url_part, 1, -1); } $parts = $this->_extract_groups($url_part); $url_part = str_replace($parts, '%', $url_part); if (preg_match_all('/(\*|:)(\w+)/', $url_part, $ms)) { foreach ($ms[1] as $k => $type) { $var_name = $ms[2][$k]; # Get constraint from properties. $constraint = isset($this->_constraints[$var_name]) ? $this->_constraints[$var_name] : null; # Get default from properties. $default = isset($this->_defaults[$var_name]) ? $this->_defaults[$var_name] : null; $this->_vars_names[] = $var_name; $this->_vars[$var_name] = array( 'type' => $type, 'constraint' => $constraint, 'default' => $default, 'value' => null, 'optional' => false ); } $vars[] = $ms[2]; } else $vars[] = []; foreach ($parts as $subpart) { if ($c = $this->_find_vars($subpart)) $vars[] = $c; } return $vars; } private function _extract_groups($str) { $parts = array(); while ($part = $this->_extract_group($str)) { $str = substr($str, strpos($str, $part) + strlen($part)); $parts[] = $part; } return $parts; } private function _extract_group($str) { if (false === ($pos = strpos($str, '('))) return; $group = ''; $open = 1; while ($open > 0) { $str = substr($str, $pos+1); $close_pos = strpos($str, ')'); $part = substr($str, 0, $close_pos+1); if (is_bool(strpos($part, '('))) { $pos = $close_pos; $group .= $part; $open--; } else { $pos = strpos($str, '('); $group .= substr($str, 0, $pos+1); $open++; } } return '(' . $group; } /** * Extracts var names and also replaces them in the regexp with their * corresponding constraints. */ private function _extract_vars() { $this->_optional_groups = $this->_find_vars(); $this->_constraints = []; } /** * Set index = action by default. */ private function _set_default_vars_values() { $default_action = 'index'; if (!$this->_assets_route) { # Action defaulted to "index" even thought _to said # otherwise. if ($this->_to) { try { $token = new UrlToken($this->_to); list($controller, $default_action) = $token->parts(); } catch (Exception $e) { } } } if (!isset($this->_defaults['action'])) $this->_defaults['action'] = $default_action; foreach ($this->_defaults as $var => $val) { if ($var == 'action') { if ($val == ':action') { $val = 'index'; } $this->_action = $val; } elseif ($var == 'controller') $this->_controller = $val; $this->_vars[$var]['value'] = $val; } if (!isset($this->_vars['format']['value']) || $this->_vars['format']['value'] === '') $this->_vars['format']['value'] = 'html'; $this->_format = $this->_vars['format']['value']; $this->_defaults = null; } /** * Check if the controller actually exists after * matching the controller and action. */ // private function _path_exists() // { // if ($this->rails_admin()) // return true; // return is_file($this->controller_file()); // } /** * Parses the "to" property, which is actually a UrlToken. * This isn't necessary to do if the route is like ':controller/:action'. */ private function _parse_token() { if (!$this->_controller || !$this->_action) { if (!$this->_action && isset($this->_vars['action'])) $this->_action = $this->_vars['action']['value']; if (!$this->_to) { if (!isset($this->_vars['controller']) || !isset($this->_vars['action'])) return false; $this->_controller = $this->_vars['controller']['value']; $this->_action = $this->_vars['action']['value']; } else { $to = $this->_has_variable_to() ? $this->_replace_variable_to($this->_controller, $this->_action) : $this->_to; try { $token = new UrlToken($to); list($this->_controller, $this->_action) = $token->parts(); } catch (Exception $e) { return false; } } } return true; } private function _is_root() { return $this->_url === $this->_root_url(); } private function _root_url() { return Mapper::ROOT_URL; } private function _replace_variable_to($controller, $action) { $parts = explode('#', $this->_to); empty($parts[1]) && $parts[1] = 'index'; $part = is_int(strpos($parts[0], ':')) ? $parts[0] : $parts[1]; preg_match('/:(\w+)/', $part, $m); $type = $m[1]; return str_replace(':'.$type, $$type, $this->_to); } /** * Returns the variable part of the _to attribute. */ private function _parse_variable_to() { $parts = explode('#', $this->_to); empty($parts[1]) && $parts[1] = 'index'; $part = is_int(strpos($parts[0], ':')) ? $parts[0] : $parts[1]; preg_match('/:(\w+)/', $part, $m); $type = $m[1]; return $type; } private function _set_alias() { if (!$this->_as) { if ($this->_is_root()) $this->_as = 'root'; elseif ($this->rails_admin() && $this->_rails_panel) { $this->_as = 'rails_panel'; } // elseif ($this->_controller && $this->_action) // $this->_as = (($namespace = $this->_namespaces) ? implode('_', $namespace) . '_' : '') . $this->_controller . '_' . $this->_action; } } private function _has_variable_to() { return is_int(strpos($this->_to, ':')); } }