123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- <?php
- /**
- * Routes are used to determine the controller and action for a requested URI.
- * Every route generates a regular expression which is used to match a URI
- * and a route. Routes may also contain keys which can be used to set the
- * controller, action, and parameters.
- *
- * Each <key> will be translated to a regular expression using a default
- * regular expression pattern. You can override the default pattern by providing
- * a pattern for the key:
- *
- * // This route will only match when <id> is a digit
- * Route::set('user', 'user/<action>/<id>', array('id' => '\d+'));
- *
- * // This route will match when <path> is anything
- * Route::set('file', '<path>', array('path' => '.*'));
- *
- * It is also possible to create optional segments by using parentheses in
- * the URI definition:
- *
- * // This is the standard default route, and no keys are required
- * Route::set('default', '(<controller>(/<action>(/<id>)))');
- *
- * // This route only requires the <file> key
- * Route::set('file', '(<path>/)<file>(.<format>)', array('path' => '.*', 'format' => '\w+'));
- *
- * Routes also provide a way to generate URIs (called "reverse routing"), which
- * makes them an extremely powerful and flexible way to generate internal links.
- *
- * @package KO7
- * @category Base
- *
- * @copyright (c) 2007-2016 Kohana Team
- * @copyright (c) since 2016 Koseven Team
- * @license https://koseven.dev/LICENSE
- */
- class KO7_Route {
- // Matches a URI group and captures the contents
- const REGEX_GROUP = '\(((?:(?>[^()]+)|(?R))*)\)';
- // Defines the pattern of a <segment>
- const REGEX_KEY = '<([a-zA-Z0-9_]++)>';
- // What can be part of a <segment> value
- const REGEX_SEGMENT = '[^/.,;?\n]++';
- // What must be escaped in the route regex
- const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]';
- /**
- * @var string default protocol for all routes
- *
- * @example 'http://'
- */
- public static $default_protocol = 'http://';
- /**
- * @var array list of valid localhost entries
- */
- public static $localhosts = [FALSE, '', 'local', 'localhost'];
- /**
- * @var string default action for all routes
- */
- public static $default_action = 'index';
- /**
- * @var bool Indicates whether routes are cached
- */
- public static $cache = FALSE;
- /**
- * @var array
- */
- protected static $_routes = [];
- /**
- * Stores a named route and returns it. The "action" will always be set to
- * "index" if it is not defined.
- *
- * Route::set('default', '(<controller>(/<action>(/<id>)))')
- * ->defaults(array(
- * 'controller' => 'welcome',
- * ));
- *
- * @param string $name route name
- * @param string $uri URI pattern
- * @param array $regex regex patterns for route keys
- * @return Route
- */
- public static function set($name, $uri = NULL, $regex = NULL)
- {
- return Route::$_routes[$name] = new Route($uri, $regex);
- }
- /**
- * Retrieves a named route.
- *
- * $route = Route::get('default');
- *
- * @param string $name route name
- * @return Route
- * @throws KO7_Exception
- */
- public static function get($name)
- {
- if ( ! isset(Route::$_routes[$name]))
- {
- throw new KO7_Exception('The requested route does not exist: :route',
- [':route' => $name]);
- }
- return Route::$_routes[$name];
- }
- /**
- * Retrieves all named routes.
- *
- * $routes = Route::all();
- *
- * @return array routes by name
- */
- public static function all()
- {
- return Route::$_routes;
- }
- /**
- * Get the name of a route.
- *
- * $name = Route::name($route)
- *
- * @param Route $route instance
- * @return string
- */
- public static function name(Route $route)
- {
- return array_search($route, Route::$_routes);
- }
- /**
- * Saves or loads the route cache. If your routes will remain the same for
- * a long period of time, use this to reload the routes from the cache
- * rather than redefining them on every page load.
- *
- * if ( ! Route::cache())
- * {
- * // Set routes here
- * Route::cache(TRUE);
- * }
- *
- * @param boolean $save cache the current routes
- * @param boolean $append append, rather than replace, cached routes when loading
- * @return void when saving routes
- * @return boolean when loading routes
- * @uses KO7::cache
- */
- public static function cache($save = FALSE, $append = FALSE)
- {
- if ($save === TRUE)
- {
- try
- {
- // Cache all defined routes
- KO7::cache('Route::cache()', Route::$_routes);
- }
- catch (Exception $e)
- {
- // We most likely have a lambda in a route, which cannot be cached
- throw new KO7_Exception('One or more routes could not be cached (:message)', [
- ':message' => $e->getMessage(),
- ], 0, $e);
- }
- }
- else
- {
- if ($routes = KO7::cache('Route::cache()'))
- {
- if ($append)
- {
- // Append cached routes
- Route::$_routes += $routes;
- }
- else
- {
- // Replace existing routes
- Route::$_routes = $routes;
- }
- // Routes were cached
- return Route::$cache = TRUE;
- }
- else
- {
- // Routes were not cached
- return Route::$cache = FALSE;
- }
- }
- }
- /**
- * Create a URL from a route name. This is a shortcut for:
- *
- * echo URL::site(Route::get($name)->uri($params), $protocol);
- *
- * @param string $name route name
- * @param array $params URI parameters
- * @param mixed $protocol protocol string or boolean, adds protocol and domain
- * @return string
- * @since 3.0.7
- * @uses URL::site
- */
- public static function url($name, array $params = NULL, $protocol = NULL)
- {
- $route = Route::get($name);
- // Create a URI with the route and convert it to a URL
- if ($route->is_external())
- return $route->uri($params);
- else
- return URL::site($route->uri($params), $protocol);
- }
- /**
- * Returns the compiled regular expression for the route. This translates
- * keys and optional groups to a proper PCRE regular expression.
- *
- * $compiled = Route::compile(
- * '<controller>(/<action>(/<id>))',
- * array(
- * 'controller' => '[a-z]+',
- * 'id' => '\d+',
- * )
- * );
- *
- * @return string
- * @uses Route::REGEX_ESCAPE
- * @uses Route::REGEX_SEGMENT
- */
- public static function compile($uri, array $regex = NULL)
- {
- // The URI should be considered literal except for keys and optional parts
- // Escape everything preg_quote would escape except for : ( ) < >
- $expression = preg_replace('#'.Route::REGEX_ESCAPE.'#', '\\\\$0', $uri);
- if (strpos($expression, '(') !== FALSE)
- {
- // Make optional parts of the URI non-capturing and optional
- $expression = str_replace(['(', ')'], ['(?:', ')?'], $expression);
- }
- // Insert default regex for keys
- $expression = str_replace(['<', '>'], ['(?P<', '>'.Route::REGEX_SEGMENT.')'], $expression);
- if ($regex)
- {
- $search = $replace = [];
- foreach ($regex as $key => $value)
- {
- $search[] = "<$key>".Route::REGEX_SEGMENT;
- $replace[] = "<$key>$value";
- }
- // Replace the default regex with the user-specified regex
- $expression = str_replace($search, $replace, $expression);
- }
- return '#^'.$expression.'$#uD';
- }
- /**
- * @var array route filters
- */
- protected $_filters = [];
- /**
- * @var string route URI
- */
- protected $_uri = '';
- /**
- * @var array
- */
- protected $_regex = [];
- /**
- * @var array
- */
- protected $_defaults = ['action' => 'index', 'host' => FALSE];
- /**
- * @var string
- */
- protected $_route_regex;
- /**
- * Creates a new route. Sets the URI and regular expressions for keys.
- * Routes should always be created with [Route::set] or they will not
- * be properly stored.
- *
- * $route = new Route($uri, $regex);
- *
- * The $uri parameter should be a string for basic regex matching.
- *
- *
- * @param string $uri route URI pattern
- * @param array $regex key patterns
- * @return void
- * @uses Route::_compile
- */
- public function __construct($uri = NULL, $regex = NULL)
- {
- if ($uri === NULL)
- {
- // Assume the route is from cache
- return;
- }
- if ( ! empty($uri))
- {
- $this->_uri = $uri;
- }
- if ( ! empty($regex))
- {
- $this->_regex = $regex;
- }
- // Store the compiled regex locally
- $this->_route_regex = Route::compile($uri, $regex);
- }
- /**
- * Provides default values for keys when they are not present. The default
- * action will always be "index" unless it is overloaded here.
- *
- * $route->defaults(array(
- * 'controller' => 'welcome',
- * 'action' => 'index'
- * ));
- *
- * If no parameter is passed, this method will act as a getter.
- *
- * @param array $defaults key values
- * @return $this or array
- */
- public function defaults(array $defaults = NULL)
- {
- if ($defaults === NULL)
- {
- return $this->_defaults;
- }
- $this->_defaults = $defaults;
- return $this;
- }
- /**
- * Filters to be run before route parameters are returned:
- *
- * $route->filter(
- * function(Route $route, $params, Request $request)
- * {
- * if ($request->method() !== HTTP_Request::POST)
- * {
- * return FALSE; // This route only matches POST requests
- * }
- * if ($params AND $params['controller'] === 'welcome')
- * {
- * $params['controller'] = 'home';
- * }
- *
- * return $params;
- * }
- * );
- *
- * To prevent a route from matching, return `FALSE`. To replace the route
- * parameters, return an array.
- *
- * [!!] Default parameters are added before filters are called!
- *
- * @throws KO7_Exception
- * @param mixed $callback callback string, array, or closure
- * @return $this
- */
- public function filter($callback)
- {
- if ( ! is_callable($callback))
- {
- throw new KO7_Exception('Invalid Route::callback specified');
- }
- $this->_filters[] = $callback;
- return $this;
- }
- /**
- * Tests if the route matches a given Request. A successful match will return
- * all of the routed parameters as an array. A failed match will return
- * boolean FALSE.
- *
- * // Params: controller = users, action = edit, id = 10
- * $params = $route->matches(Request::factory('users/edit/10'));
- *
- * This method should almost always be used within an if/else block:
- *
- * if ($params = $route->matches($request))
- * {
- * // Parse the parameters
- * }
- *
- * @param Request $request Request object to match
- * @return array on success
- * @return FALSE on failure
- */
- public function matches(Request $request)
- {
- // Get the URI from the Request
- $uri = trim($request->uri(), '/');
- if ( ! preg_match($this->_route_regex, $uri, $matches))
- return FALSE;
- $params = [];
- foreach ($matches as $key => $value)
- {
- if (is_int($key))
- {
- // Skip all unnamed keys
- continue;
- }
- // Set the value for all matched keys
- $params[$key] = $value;
- }
- foreach ($this->_defaults as $key => $value)
- {
- if ( ! isset($params[$key]) OR $params[$key] === '')
- {
- // Set default values for any key that was not matched
- $params[$key] = $value;
- }
- }
- if ( ! empty($params['controller']))
- {
- // PSR-0: Replace underscores with spaces, run ucwords, then replace underscore
- $params['controller'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $params['controller'])));
- }
- if ( ! empty($params['directory']))
- {
- // PSR-0: Replace underscores with spaces, run ucwords, then replace underscore
- $params['directory'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $params['directory'])));
- }
- if ($this->_filters)
- {
- foreach ($this->_filters as $callback)
- {
- // Execute the filter giving it the route, params, and request
- $return = call_user_func($callback, $this, $params, $request);
- if ($return === FALSE)
- {
- // Filter has aborted the match
- return FALSE;
- }
- elseif (is_array($return))
- {
- // Filter has modified the parameters
- $params = $return;
- }
- }
- }
- return $params;
- }
- /**
- * Returns whether this route is an external route
- * to a remote controller.
- *
- * @return boolean
- */
- public function is_external()
- {
- return ! in_array(Arr::get($this->_defaults, 'host', FALSE), Route::$localhosts);
- }
- /**
- * Generates a URI for the current route based on the parameters given.
- *
- * // Using the "default" route: "users/profile/10"
- * $route->uri(array(
- * 'controller' => 'users',
- * 'action' => 'profile',
- * 'id' => '10'
- * ));
- *
- * @param array $params URI parameters
- * @return string
- * @throws KO7_Exception
- * @uses Route::REGEX_GROUP
- * @uses Route::REGEX_KEY
- */
- public function uri(array $params = NULL)
- {
- if ($params)
- {
- // @issue #4079 rawurlencode parameters
- $params = array_map('rawurlencode', $params);
- // decode slashes back, see Apache docs about AllowEncodedSlashes and AcceptPathInfo
- $params = str_replace(['%2F', '%5C'], ['/', '\\'], $params);
- }
- $defaults = $this->_defaults;
- /**
- * Recursively compiles a portion of a URI specification by replacing
- * the specified parameters and any optional parameters that are needed.
- *
- * @param string $portion Part of the URI specification
- * @param boolean $required Whether or not parameters are required (initially)
- * @return array Tuple of the compiled portion and whether or not it contained specified parameters
- */
- $compile = function ($portion, $required) use (&$compile, $defaults, $params)
- {
- $missing = [];
- $pattern = '#(?:'.Route::REGEX_KEY.'|'.Route::REGEX_GROUP.')#';
- $result = preg_replace_callback($pattern, function ($matches) use (&$compile, $defaults, &$missing, $params, &$required)
- {
- if ($matches[0][0] === '<')
- {
- // Parameter, unwrapped
- $param = $matches[1];
- if (isset($params[$param]))
- {
- // This portion is required when a specified
- // parameter does not match the default
- $required = ($required OR ! isset($defaults[$param]) OR $params[$param] !== $defaults[$param]);
- // Add specified parameter to this result
- return $params[$param];
- }
- // Add default parameter to this result
- if (isset($defaults[$param]))
- return $defaults[$param];
- // This portion is missing a parameter
- $missing[] = $param;
- }
- else
- {
- // Group, unwrapped
- $result = $compile($matches[2], FALSE);
- if ($result[1])
- {
- // This portion is required when it contains a group
- // that is required
- $required = TRUE;
- // Add required groups to this result
- return $result[0];
- }
- // Do not add optional groups to this result
- }
- }, $portion);
- if ($required AND $missing)
- {
- throw new KO7_Exception(
- 'Required route parameter not passed: :param',
- [':param' => reset($missing)]
- );
- }
- return [$result, $required];
- };
- list($uri) = $compile($this->_uri, TRUE);
- // Trim all extra slashes from the URI
- $uri = preg_replace('#//+#', '/', rtrim($uri, '/'));
- if ($this->is_external())
- {
- // Need to add the host to the URI
- $host = $this->_defaults['host'];
- if (strpos($host, '://') === FALSE)
- {
- // Use the default defined protocol
- $host = Route::$default_protocol.$host;
- }
- // Clean up the host and prepend it to the URI
- $uri = rtrim($host, '/').'/'.$uri;
- }
- return $uri;
- }
- }
|