1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 0.2.9
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Routing;
16:
17: use Cake\Core\Configure;
18: use Cake\Http\ServerRequest;
19: use Cake\Routing\Exception\MissingRouteException;
20: use Cake\Utility\Inflector;
21: use Exception;
22: use Psr\Http\Message\ServerRequestInterface;
23: use ReflectionFunction;
24: use ReflectionMethod;
25: use RuntimeException;
26: use Throwable;
27:
28: /**
29: * Parses the request URL into controller, action, and parameters. Uses the connected routes
30: * to match the incoming URL string to parameters that will allow the request to be dispatched. Also
31: * handles converting parameter lists into URL strings, using the connected routes. Routing allows you to decouple
32: * the way the world interacts with your application (URLs) and the implementation (controllers and actions).
33: *
34: * ### Connecting routes
35: *
36: * Connecting routes is done using Router::connect(). When parsing incoming requests or reverse matching
37: * parameters, routes are enumerated in the order they were connected. For more information on routes and
38: * how to connect them see Router::connect().
39: */
40: class Router
41: {
42:
43: /**
44: * Have routes been loaded
45: *
46: * @var bool
47: * @deprecated 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0
48: */
49: public static $initialized = false;
50:
51: /**
52: * Default route class.
53: *
54: * @var string
55: */
56: protected static $_defaultRouteClass = 'Cake\Routing\Route\Route';
57:
58: /**
59: * Contains the base string that will be applied to all generated URLs
60: * For example `https://example.com`
61: *
62: * @var string
63: */
64: protected static $_fullBaseUrl;
65:
66: /**
67: * Regular expression for action names
68: *
69: * @var string
70: */
71: const ACTION = 'index|show|add|create|edit|update|remove|del|delete|view|item';
72:
73: /**
74: * Regular expression for years
75: *
76: * @var string
77: */
78: const YEAR = '[12][0-9]{3}';
79:
80: /**
81: * Regular expression for months
82: *
83: * @var string
84: */
85: const MONTH = '0[1-9]|1[012]';
86:
87: /**
88: * Regular expression for days
89: *
90: * @var string
91: */
92: const DAY = '0[1-9]|[12][0-9]|3[01]';
93:
94: /**
95: * Regular expression for auto increment IDs
96: *
97: * @var string
98: */
99: const ID = '[0-9]+';
100:
101: /**
102: * Regular expression for UUIDs
103: *
104: * @var string
105: */
106: const UUID = '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}';
107:
108: /**
109: * The route collection routes would be added to.
110: *
111: * @var \Cake\Routing\RouteCollection
112: */
113: protected static $_collection;
114:
115: /**
116: * A hash of request context data.
117: *
118: * @var array
119: */
120: protected static $_requestContext = [];
121:
122: /**
123: * Named expressions
124: *
125: * @var array
126: */
127: protected static $_namedExpressions = [
128: 'Action' => Router::ACTION,
129: 'Year' => Router::YEAR,
130: 'Month' => Router::MONTH,
131: 'Day' => Router::DAY,
132: 'ID' => Router::ID,
133: 'UUID' => Router::UUID
134: ];
135:
136: /**
137: * Maintains the request object stack for the current request.
138: * This will contain more than one request object when requestAction is used.
139: *
140: * @var array
141: */
142: protected static $_requests = [];
143:
144: /**
145: * Initial state is populated the first time reload() is called which is at the bottom
146: * of this file. This is a cheat as get_class_vars() returns the value of static vars even if they
147: * have changed.
148: *
149: * @var array
150: */
151: protected static $_initialState = [];
152:
153: /**
154: * The stack of URL filters to apply against routing URLs before passing the
155: * parameters to the route collection.
156: *
157: * @var callable[]
158: */
159: protected static $_urlFilters = [];
160:
161: /**
162: * Default extensions defined with Router::extensions()
163: *
164: * @var array
165: */
166: protected static $_defaultExtensions = [];
167:
168: /**
169: * Get or set default route class.
170: *
171: * @param string|null $routeClass Class name.
172: * @return string|null
173: */
174: public static function defaultRouteClass($routeClass = null)
175: {
176: if ($routeClass === null) {
177: return static::$_defaultRouteClass;
178: }
179: static::$_defaultRouteClass = $routeClass;
180: }
181:
182: /**
183: * Gets the named route patterns for use in config/routes.php
184: *
185: * @return array Named route elements
186: * @see \Cake\Routing\Router::$_namedExpressions
187: */
188: public static function getNamedExpressions()
189: {
190: return static::$_namedExpressions;
191: }
192:
193: /**
194: * Connects a new Route in the router.
195: *
196: * Compatibility proxy to \Cake\Routing\RouteBuilder::connect() in the `/` scope.
197: *
198: * @param string $route A string describing the template of the route
199: * @param array|string $defaults An array describing the default route parameters. These parameters will be used by default
200: * and can supply routing parameters that are not dynamic. See above.
201: * @param array $options An array matching the named elements in the route to regular expressions which that
202: * element should match. Also contains additional parameters such as which routed parameters should be
203: * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a
204: * custom routing class.
205: * @return void
206: * @throws \Cake\Core\Exception\Exception
207: * @see \Cake\Routing\RouteBuilder::connect()
208: * @see \Cake\Routing\Router::scope()
209: */
210: public static function connect($route, $defaults = [], $options = [])
211: {
212: static::$initialized = true;
213: static::scope('/', function ($routes) use ($route, $defaults, $options) {
214: /** @var \Cake\Routing\RouteBuilder $routes */
215: $routes->connect($route, $defaults, $options);
216: });
217: }
218:
219: /**
220: * Connects a new redirection Route in the router.
221: *
222: * Compatibility proxy to \Cake\Routing\RouteBuilder::redirect() in the `/` scope.
223: *
224: * @param string $route A string describing the template of the route
225: * @param array|string $url A URL to redirect to. Can be a string or a Cake array-based URL
226: * @param array $options An array matching the named elements in the route to regular expressions which that
227: * element should match. Also contains additional parameters such as which routed parameters should be
228: * shifted into the passed arguments. As well as supplying patterns for routing parameters.
229: * @return void
230: * @see \Cake\Routing\RouteBuilder::redirect()
231: * @deprecated 3.3.0 Use Router::scope() and RouteBuilder::redirect() instead.
232: */
233: public static function redirect($route, $url, $options = [])
234: {
235: deprecationWarning(
236: 'Router::redirect() is deprecated. ' .
237: 'Use Router::scope() and RouteBuilder::redirect() instead.'
238: );
239: if (is_string($url)) {
240: $url = ['redirect' => $url];
241: }
242: if (!isset($options['routeClass'])) {
243: $options['routeClass'] = 'Cake\Routing\Route\RedirectRoute';
244: }
245: static::connect($route, $url, $options);
246: }
247:
248: /**
249: * Generate REST resource routes for the given controller(s).
250: *
251: * Compatibility proxy to \Cake\Routing\RouteBuilder::resources(). Additional, compatibility
252: * around prefixes and plugins and prefixes is handled by this method.
253: *
254: * A quick way to generate a default routes to a set of REST resources (controller(s)).
255: *
256: * ### Usage
257: *
258: * Connect resource routes for an app controller:
259: *
260: * ```
261: * Router::mapResources('Posts');
262: * ```
263: *
264: * Connect resource routes for the Comment controller in the
265: * Comments plugin:
266: *
267: * ```
268: * Router::mapResources('Comments.Comment');
269: * ```
270: *
271: * Plugins will create lower_case underscored resource routes. e.g
272: * `/comments/comment`
273: *
274: * Connect resource routes for the Posts controller in the
275: * Admin prefix:
276: *
277: * ```
278: * Router::mapResources('Posts', ['prefix' => 'admin']);
279: * ```
280: *
281: * Prefixes will create lower_case underscored resource routes. e.g
282: * `/admin/posts`
283: *
284: * ### Options:
285: *
286: * - 'id' - The regular expression fragment to use when matching IDs. By default, matches
287: * integer values and UUIDs.
288: * - 'prefix' - Routing prefix to use for the generated routes. Defaults to ''.
289: * Using this option will create prefixed routes, similar to using Routing.prefixes.
290: * - 'only' - Only connect the specific list of actions.
291: * - 'actions' - Override the method names used for connecting actions.
292: * - 'map' - Additional resource routes that should be connected. If you define 'only' and 'map',
293: * make sure that your mapped methods are also in the 'only' list.
294: * - 'path' - Change the path so it doesn't match the resource name. E.g ArticlesController
295: * is available at `/posts`
296: *
297: * @param string|array $controller A controller name or array of controller names (i.e. "Posts" or "ListItems")
298: * @param array $options Options to use when generating REST routes
299: * @see \Cake\Routing\RouteBuilder::resources()
300: * @deprecated 3.3.0 Use Router::scope() and RouteBuilder::resources() instead.
301: * @return void
302: */
303: public static function mapResources($controller, $options = [])
304: {
305: deprecationWarning(
306: 'Router::mapResources() is deprecated. ' .
307: 'Use Router::scope() and RouteBuilder::resources() instead.'
308: );
309: foreach ((array)$controller as $name) {
310: list($plugin, $name) = pluginSplit($name);
311:
312: $prefix = $pluginUrl = false;
313: if (!empty($options['prefix'])) {
314: $prefix = $options['prefix'];
315: unset($options['prefix']);
316: }
317: if ($plugin) {
318: $pluginUrl = Inflector::underscore($plugin);
319: }
320:
321: $callback = function ($routes) use ($name, $options) {
322: /** @var \Cake\Routing\RouteBuilder $routes */
323: $routes->resources($name, $options);
324: };
325:
326: if ($plugin && $prefix) {
327: $path = '/' . implode('/', [$prefix, $pluginUrl]);
328: $params = ['prefix' => $prefix, 'plugin' => $plugin];
329: static::scope($path, $params, $callback);
330:
331: return;
332: }
333:
334: if ($prefix) {
335: static::prefix($prefix, $callback);
336:
337: return;
338: }
339:
340: if ($plugin) {
341: static::plugin($plugin, $callback);
342:
343: return;
344: }
345:
346: static::scope('/', $callback);
347:
348: return;
349: }
350: }
351:
352: /**
353: * Parses given URL string. Returns 'routing' parameters for that URL.
354: *
355: * @param string $url URL to be parsed.
356: * @param string $method The HTTP method being used.
357: * @return array Parsed elements from URL.
358: * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
359: * @deprecated 3.4.0 Use Router::parseRequest() instead.
360: */
361: public static function parse($url, $method = '')
362: {
363: deprecationWarning(
364: 'Router::parse() is deprecated. ' .
365: 'Use Router::parseRequest() instead. This will require adopting the Http\Server library.'
366: );
367: if (!static::$initialized) {
368: static::_loadRoutes();
369: }
370: if (strpos($url, '/') !== 0) {
371: $url = '/' . $url;
372: }
373:
374: return static::$_collection->parse($url, $method);
375: }
376:
377: /**
378: * Get the routing parameters for the request is possible.
379: *
380: * @param \Psr\Http\Message\ServerRequestInterface $request The request to parse request data from.
381: * @return array Parsed elements from URL.
382: * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
383: */
384: public static function parseRequest(ServerRequestInterface $request)
385: {
386: if (!static::$initialized) {
387: static::_loadRoutes();
388: }
389:
390: return static::$_collection->parseRequest($request);
391: }
392:
393: /**
394: * Takes parameter and path information back from the Dispatcher, sets these
395: * parameters as the current request parameters that are merged with URL arrays
396: * created later in the request.
397: *
398: * Nested requests will create a stack of requests. You can remove requests using
399: * Router::popRequest(). This is done automatically when using Object::requestAction().
400: *
401: * Will accept either a Cake\Http\ServerRequest object or an array of arrays. Support for
402: * accepting arrays may be removed in the future.
403: *
404: * @param \Cake\Http\ServerRequest|array $request Parameters and path information or a Cake\Http\ServerRequest object.
405: * @return void
406: * @deprecatd 3.6.0 Support for arrays will be removed in 4.0.0
407: */
408: public static function setRequestInfo($request)
409: {
410: if ($request instanceof ServerRequest) {
411: static::pushRequest($request);
412: } else {
413: deprecationWarning(
414: 'Passing an array into Router::setRequestInfo() is deprecated. ' .
415: 'Pass an instance of ServerRequest instead.'
416: );
417:
418: $requestData = $request;
419: $requestData += [[], []];
420: $requestData[0] += [
421: 'controller' => false,
422: 'action' => false,
423: 'plugin' => null
424: ];
425: $request = new ServerRequest([
426: 'params' => $requestData[0],
427: 'url' => isset($requestData[1]['here']) ? $requestData[1]['here'] : '/',
428: 'base' => isset($requestData[1]['base']) ? $requestData[1]['base'] : '',
429: 'webroot' => isset($requestData[1]['webroot']) ? $requestData[1]['webroot'] : '/',
430: ]);
431: static::pushRequest($request);
432: }
433: }
434:
435: /**
436: * Push a request onto the request stack. Pushing a request
437: * sets the request context used when generating URLs.
438: *
439: * @param \Cake\Http\ServerRequest $request Request instance.
440: * @return void
441: */
442: public static function pushRequest(ServerRequest $request)
443: {
444: static::$_requests[] = $request;
445: static::setRequestContext($request);
446: }
447:
448: /**
449: * Store the request context for a given request.
450: *
451: * @param \Psr\Http\Message\ServerRequestInterface $request The request instance.
452: * @return void
453: * @throws \InvalidArgumentException When parameter is an incorrect type.
454: */
455: public static function setRequestContext(ServerRequestInterface $request)
456: {
457: $uri = $request->getUri();
458: static::$_requestContext = [
459: '_base' => $request->getAttribute('base'),
460: '_port' => $uri->getPort(),
461: '_scheme' => $uri->getScheme(),
462: '_host' => $uri->getHost(),
463: ];
464: }
465:
466: /**
467: * Pops a request off of the request stack. Used when doing requestAction
468: *
469: * @return \Cake\Http\ServerRequest The request removed from the stack.
470: * @see \Cake\Routing\Router::pushRequest()
471: * @see \Cake\Routing\RequestActionTrait::requestAction()
472: */
473: public static function popRequest()
474: {
475: $removed = array_pop(static::$_requests);
476: $last = end(static::$_requests);
477: if ($last) {
478: static::setRequestContext($last);
479: reset(static::$_requests);
480: }
481:
482: return $removed;
483: }
484:
485: /**
486: * Get the current request object, or the first one.
487: *
488: * @param bool $current True to get the current request, or false to get the first one.
489: * @return \Cake\Http\ServerRequest|null
490: */
491: public static function getRequest($current = false)
492: {
493: if ($current) {
494: $request = end(static::$_requests);
495:
496: return $request ?: null;
497: }
498:
499: return isset(static::$_requests[0]) ? static::$_requests[0] : null;
500: }
501:
502: /**
503: * Reloads default Router settings. Resets all class variables and
504: * removes all connected routes.
505: *
506: * @return void
507: */
508: public static function reload()
509: {
510: if (empty(static::$_initialState)) {
511: static::$_collection = new RouteCollection();
512: static::$_initialState = get_class_vars(get_called_class());
513:
514: return;
515: }
516: foreach (static::$_initialState as $key => $val) {
517: if ($key !== '_initialState') {
518: static::${$key} = $val;
519: }
520: }
521: static::$_collection = new RouteCollection();
522: }
523:
524: /**
525: * Reset routes and related state.
526: *
527: * Similar to reload() except that this doesn't reset all global state,
528: * as that leads to incorrect behavior in some plugin test case scenarios.
529: *
530: * This method will reset:
531: *
532: * - routes
533: * - URL Filters
534: * - the initialized property
535: *
536: * Extensions and default route classes will not be modified
537: *
538: * @internal
539: * @return void
540: */
541: public static function resetRoutes()
542: {
543: static::$_collection = new RouteCollection();
544: static::$_urlFilters = [];
545: static::$initialized = false;
546: }
547:
548: /**
549: * Add a URL filter to Router.
550: *
551: * URL filter functions are applied to every array $url provided to
552: * Router::url() before the URLs are sent to the route collection.
553: *
554: * Callback functions should expect the following parameters:
555: *
556: * - `$params` The URL params being processed.
557: * - `$request` The current request.
558: *
559: * The URL filter function should *always* return the params even if unmodified.
560: *
561: * ### Usage
562: *
563: * URL filters allow you to easily implement features like persistent parameters.
564: *
565: * ```
566: * Router::addUrlFilter(function ($params, $request) {
567: * if ($request->getParam('lang') && !isset($params['lang'])) {
568: * $params['lang'] = $request->getParam('lang');
569: * }
570: * return $params;
571: * });
572: * ```
573: *
574: * @param callable $function The function to add
575: * @return void
576: */
577: public static function addUrlFilter(callable $function)
578: {
579: static::$_urlFilters[] = $function;
580: }
581:
582: /**
583: * Applies all the connected URL filters to the URL.
584: *
585: * @param array $url The URL array being modified.
586: * @return array The modified URL.
587: * @see \Cake\Routing\Router::url()
588: * @see \Cake\Routing\Router::addUrlFilter()
589: */
590: protected static function _applyUrlFilters($url)
591: {
592: $request = static::getRequest(true);
593: $e = null;
594: foreach (static::$_urlFilters as $filter) {
595: try {
596: $url = $filter($url, $request);
597: } catch (Exception $e) {
598: // fall through
599: } catch (Throwable $e) {
600: // fall through
601: }
602: if ($e !== null) {
603: if (is_array($filter)) {
604: $ref = new ReflectionMethod($filter[0], $filter[1]);
605: } else {
606: $ref = new ReflectionFunction($filter);
607: }
608: $message = sprintf(
609: 'URL filter defined in %s on line %s could not be applied. The filter failed with: %s',
610: $ref->getFileName(),
611: $ref->getStartLine(),
612: $e->getMessage()
613: );
614: throw new RuntimeException($message, $e->getCode(), $e);
615: }
616: }
617:
618: return $url;
619: }
620:
621: /**
622: * Finds URL for specified action.
623: *
624: * Returns a URL pointing to a combination of controller and action.
625: *
626: * ### Usage
627: *
628: * - `Router::url('/posts/edit/1');` Returns the string with the base dir prepended.
629: * This usage does not use reverser routing.
630: * - `Router::url(['controller' => 'posts', 'action' => 'edit']);` Returns a URL
631: * generated through reverse routing.
632: * - `Router::url(['_name' => 'custom-name', ...]);` Returns a URL generated
633: * through reverse routing. This form allows you to leverage named routes.
634: *
635: * There are a few 'special' parameters that can change the final URL string that is generated
636: *
637: * - `_base` - Set to false to remove the base path from the generated URL. If your application
638: * is not in the root directory, this can be used to generate URLs that are 'cake relative'.
639: * cake relative URLs are required when using requestAction.
640: * - `_scheme` - Set to create links on different schemes like `webcal` or `ftp`. Defaults
641: * to the current scheme.
642: * - `_host` - Set the host to use for the link. Defaults to the current host.
643: * - `_port` - Set the port if you need to create links on non-standard ports.
644: * - `_full` - If true output of `Router::fullBaseUrl()` will be prepended to generated URLs.
645: * - `#` - Allows you to set URL hash fragments.
646: * - `_ssl` - Set to true to convert the generated URL to https, or false to force http.
647: * - `_name` - Name of route. If you have setup named routes you can use this key
648: * to specify it.
649: *
650: * @param string|array|null $url An array specifying any of the following:
651: * 'controller', 'action', 'plugin' additionally, you can provide routed
652: * elements or query string parameters. If string it can be name any valid url
653: * string.
654: * @param bool $full If true, the full base URL will be prepended to the result.
655: * Default is false.
656: * @return string Full translated URL with base path.
657: * @throws \Cake\Core\Exception\Exception When the route name is not found
658: */
659: public static function url($url = null, $full = false)
660: {
661: if (!static::$initialized) {
662: static::_loadRoutes();
663: }
664:
665: $params = [
666: 'plugin' => null,
667: 'controller' => null,
668: 'action' => 'index',
669: '_ext' => null,
670: ];
671: $here = $output = $frag = null;
672:
673: $context = static::$_requestContext;
674: // In 4.x this should be replaced with state injected via setRequestContext
675: $request = static::getRequest(true);
676: if ($request) {
677: $params = $request->getAttribute('params');
678: $here = $request->getRequestTarget();
679: $context['_base'] = $request->getAttribute('base');
680: } elseif (!isset($context['_base'])) {
681: $context['_base'] = Configure::read('App.base');
682: }
683:
684: if (empty($url)) {
685: $output = $context['_base'] . (isset($here) ? $here : '/');
686: if ($full) {
687: $output = static::fullBaseUrl() . $output;
688: }
689:
690: return $output;
691: }
692: if (is_array($url)) {
693: if (isset($url['_ssl'])) {
694: $url['_scheme'] = ($url['_ssl'] === true) ? 'https' : 'http';
695: }
696:
697: if (isset($url['_full']) && $url['_full'] === true) {
698: $full = true;
699: }
700: if (isset($url['#'])) {
701: $frag = '#' . $url['#'];
702: }
703: unset($url['_ssl'], $url['_full'], $url['#']);
704:
705: $url = static::_applyUrlFilters($url);
706:
707: if (!isset($url['_name'])) {
708: // Copy the current action if the controller is the current one.
709: if (empty($url['action']) &&
710: (empty($url['controller']) || $params['controller'] === $url['controller'])
711: ) {
712: $url['action'] = $params['action'];
713: }
714:
715: // Keep the current prefix around if none set.
716: if (isset($params['prefix']) && !isset($url['prefix'])) {
717: $url['prefix'] = $params['prefix'];
718: }
719:
720: $url += [
721: 'plugin' => $params['plugin'],
722: 'controller' => $params['controller'],
723: 'action' => 'index',
724: '_ext' => null
725: ];
726: }
727:
728: // If a full URL is requested with a scheme the host should default
729: // to App.fullBaseUrl to avoid corrupt URLs
730: if ($full && isset($url['_scheme']) && !isset($url['_host'])) {
731: $url['_host'] = parse_url(static::fullBaseUrl(), PHP_URL_HOST);
732: }
733: $context['params'] = $params;
734:
735: $output = static::$_collection->match($url, $context);
736: } else {
737: $plainString = (
738: strpos($url, 'javascript:') === 0 ||
739: strpos($url, 'mailto:') === 0 ||
740: strpos($url, 'tel:') === 0 ||
741: strpos($url, 'sms:') === 0 ||
742: strpos($url, '#') === 0 ||
743: strpos($url, '?') === 0 ||
744: strpos($url, '//') === 0 ||
745: strpos($url, '://') !== false
746: );
747:
748: if ($plainString) {
749: return $url;
750: }
751: $output = $context['_base'] . $url;
752: }
753: $protocol = preg_match('#^[a-z][a-z0-9+\-.]*\://#i', $output);
754: if ($protocol === 0) {
755: $output = str_replace('//', '/', '/' . $output);
756: if ($full) {
757: $output = static::fullBaseUrl() . $output;
758: }
759: }
760:
761: return $output . $frag;
762: }
763:
764: /**
765: * Finds URL for specified action.
766: *
767: * Returns a bool if the url exists
768: *
769: * ### Usage
770: *
771: * @see Router::url()
772: *
773: * @param string|array|null $url An array specifying any of the following:
774: * 'controller', 'action', 'plugin' additionally, you can provide routed
775: * elements or query string parameters. If string it can be name any valid url
776: * string.
777: * @param bool $full If true, the full base URL will be prepended to the result.
778: * Default is false.
779: * @return bool
780: */
781: public static function routeExists($url = null, $full = false)
782: {
783: try {
784: $route = static::url($url, $full);
785:
786: return true;
787: } catch (MissingRouteException $e) {
788: return false;
789: }
790: }
791:
792: /**
793: * Sets the full base URL that will be used as a prefix for generating
794: * fully qualified URLs for this application. If no parameters are passed,
795: * the currently configured value is returned.
796: *
797: * ### Note:
798: *
799: * If you change the configuration value `App.fullBaseUrl` during runtime
800: * and expect the router to produce links using the new setting, you are
801: * required to call this method passing such value again.
802: *
803: * @param string|null $base the prefix for URLs generated containing the domain.
804: * For example: `http://example.com`
805: * @return string
806: */
807: public static function fullBaseUrl($base = null)
808: {
809: if ($base !== null) {
810: static::$_fullBaseUrl = $base;
811: Configure::write('App.fullBaseUrl', $base);
812: }
813: if (empty(static::$_fullBaseUrl)) {
814: static::$_fullBaseUrl = Configure::read('App.fullBaseUrl');
815: }
816:
817: return static::$_fullBaseUrl;
818: }
819:
820: /**
821: * Reverses a parsed parameter array into an array.
822: *
823: * Works similarly to Router::url(), but since parsed URL's contain additional
824: * 'pass' as well as 'url.url' keys. Those keys need to be specially
825: * handled in order to reverse a params array into a string URL.
826: *
827: * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those
828: * are used for CakePHP internals and should not normally be part of an output URL.
829: *
830: * @param \Cake\Http\ServerRequest|array $params The params array or
831: * Cake\Http\ServerRequest object that needs to be reversed.
832: * @return array The URL array ready to be used for redirect or HTML link.
833: */
834: public static function reverseToArray($params)
835: {
836: $url = [];
837: if ($params instanceof ServerRequest) {
838: $url = $params->getQueryParams();
839: $params = $params->getAttribute('params');
840: } elseif (isset($params['url'])) {
841: $url = $params['url'];
842: }
843: $pass = isset($params['pass']) ? $params['pass'] : [];
844:
845: unset(
846: $params['pass'],
847: $params['paging'],
848: $params['models'],
849: $params['url'],
850: $url['url'],
851: $params['autoRender'],
852: $params['bare'],
853: $params['requested'],
854: $params['return'],
855: $params['_Token'],
856: $params['_matchedRoute'],
857: $params['_name']
858: );
859: $params = array_merge($params, $pass);
860: if (!empty($url)) {
861: $params['?'] = $url;
862: }
863:
864: return $params;
865: }
866:
867: /**
868: * Reverses a parsed parameter array into a string.
869: *
870: * Works similarly to Router::url(), but since parsed URL's contain additional
871: * 'pass' as well as 'url.url' keys. Those keys need to be specially
872: * handled in order to reverse a params array into a string URL.
873: *
874: * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those
875: * are used for CakePHP internals and should not normally be part of an output URL.
876: *
877: * @param \Cake\Http\ServerRequest|array $params The params array or
878: * Cake\Http\ServerRequest object that needs to be reversed.
879: * @param bool $full Set to true to include the full URL including the
880: * protocol when reversing the URL.
881: * @return string The string that is the reversed result of the array
882: */
883: public static function reverse($params, $full = false)
884: {
885: $params = static::reverseToArray($params);
886:
887: return static::url($params, $full);
888: }
889:
890: /**
891: * Normalizes a URL for purposes of comparison.
892: *
893: * Will strip the base path off and replace any double /'s.
894: * It will not unify the casing and underscoring of the input value.
895: *
896: * @param array|string $url URL to normalize Either an array or a string URL.
897: * @return string Normalized URL
898: */
899: public static function normalize($url = '/')
900: {
901: if (is_array($url)) {
902: $url = static::url($url);
903: }
904: if (preg_match('/^[a-z\-]+:\/\//', $url)) {
905: return $url;
906: }
907: $request = static::getRequest();
908:
909: if ($request) {
910: $base = $request->getAttribute('base');
911: if (strlen($base) && stristr($url, $base)) {
912: $url = preg_replace('/^' . preg_quote($base, '/') . '/', '', $url, 1);
913: }
914: }
915: $url = '/' . $url;
916:
917: while (strpos($url, '//') !== false) {
918: $url = str_replace('//', '/', $url);
919: }
920: $url = preg_replace('/(?:(\/$))/', '', $url);
921:
922: if (empty($url)) {
923: return '/';
924: }
925:
926: return $url;
927: }
928:
929: /**
930: * Get or set valid extensions for all routes connected later.
931: *
932: * Instructs the router to parse out file extensions
933: * from the URL. For example, http://example.com/posts.rss would yield a file
934: * extension of "rss". The file extension itself is made available in the
935: * controller as `$this->request->getParam('_ext')`, and is used by the RequestHandler
936: * component to automatically switch to alternate layouts and templates, and
937: * load helpers corresponding to the given content, i.e. RssHelper. Switching
938: * layouts and helpers requires that the chosen extension has a defined mime type
939: * in `Cake\Http\Response`.
940: *
941: * A string or an array of valid extensions can be passed to this method.
942: * If called without any parameters it will return current list of set extensions.
943: *
944: * @param array|string|null $extensions List of extensions to be added.
945: * @param bool $merge Whether to merge with or override existing extensions.
946: * Defaults to `true`.
947: * @return array Array of extensions Router is configured to parse.
948: */
949: public static function extensions($extensions = null, $merge = true)
950: {
951: $collection = static::$_collection;
952: if ($extensions === null) {
953: if (!static::$initialized) {
954: static::_loadRoutes();
955: }
956:
957: return array_unique(array_merge(static::$_defaultExtensions, $collection->getExtensions()));
958: }
959: $extensions = (array)$extensions;
960: if ($merge) {
961: $extensions = array_unique(array_merge(static::$_defaultExtensions, $extensions));
962: }
963:
964: return static::$_defaultExtensions = $extensions;
965: }
966:
967: /**
968: * Provides legacy support for named parameters on incoming URLs.
969: *
970: * Checks the passed parameters for elements containing `$options['separator']`
971: * Those parameters are split and parsed as if they were old style named parameters.
972: *
973: * The parsed parameters will be moved from params['pass'] to params['named'].
974: *
975: * ### Options
976: *
977: * - `separator` The string to use as a separator. Defaults to `:`.
978: *
979: * @param \Cake\Http\ServerRequest $request The request object to modify.
980: * @param array $options The array of options.
981: * @return \Cake\Http\ServerRequest The modified request
982: * @deprecated 3.3.0 Named parameter backwards compatibility will be removed in 4.0.
983: */
984: public static function parseNamedParams(ServerRequest $request, array $options = [])
985: {
986: deprecationWarning(
987: 'Router::parseNamedParams() is deprecated. ' .
988: '2.x backwards compatible named parameter support will be removed in 4.0'
989: );
990: $options += ['separator' => ':'];
991: if (!$request->getParam('pass')) {
992: return $request->withParam('named', []);
993: }
994: $named = [];
995: $pass = $request->getParam('pass');
996: foreach ((array)$pass as $key => $value) {
997: if (strpos($value, $options['separator']) === false) {
998: continue;
999: }
1000: unset($pass[$key]);
1001: list($key, $value) = explode($options['separator'], $value, 2);
1002:
1003: if (preg_match_all('/\[([A-Za-z0-9_-]+)?\]/', $key, $matches, PREG_SET_ORDER)) {
1004: $matches = array_reverse($matches);
1005: $parts = explode('[', $key);
1006: $key = array_shift($parts);
1007: $arr = $value;
1008: foreach ($matches as $match) {
1009: if (empty($match[1])) {
1010: $arr = [$arr];
1011: } else {
1012: $arr = [
1013: $match[1] => $arr
1014: ];
1015: }
1016: }
1017: $value = $arr;
1018: }
1019: $named = array_merge_recursive($named, [$key => $value]);
1020: }
1021:
1022: return $request
1023: ->withParam('pass', $pass)
1024: ->withParam('named', $named);
1025: }
1026:
1027: /**
1028: * Create a RouteBuilder for the provided path.
1029: *
1030: * @param string $path The path to set the builder to.
1031: * @param array $options The options for the builder
1032: * @return \Cake\Routing\RouteBuilder
1033: */
1034: public static function createRouteBuilder($path, array $options = [])
1035: {
1036: $defaults = [
1037: 'routeClass' => static::defaultRouteClass(),
1038: 'extensions' => static::$_defaultExtensions,
1039: ];
1040: $options += $defaults;
1041:
1042: return new RouteBuilder(static::$_collection, $path, [], [
1043: 'routeClass' => $options['routeClass'],
1044: 'extensions' => $options['extensions'],
1045: ]);
1046: }
1047:
1048: /**
1049: * Create a routing scope.
1050: *
1051: * Routing scopes allow you to keep your routes DRY and avoid repeating
1052: * common path prefixes, and or parameter sets.
1053: *
1054: * Scoped collections will be indexed by path for faster route parsing. If you
1055: * re-open or re-use a scope the connected routes will be merged with the
1056: * existing ones.
1057: *
1058: * ### Options
1059: *
1060: * The `$params` array allows you to define options for the routing scope.
1061: * The options listed below *are not* available to be used as routing defaults
1062: *
1063: * - `routeClass` The route class to use in this scope. Defaults to
1064: * `Router::defaultRouteClass()`
1065: * - `extensions` The extensions to enable in this scope. Defaults to the globally
1066: * enabled extensions set with `Router::extensions()`
1067: *
1068: * ### Example
1069: *
1070: * ```
1071: * Router::scope('/blog', ['plugin' => 'Blog'], function ($routes) {
1072: * $routes->connect('/', ['controller' => 'Articles']);
1073: * });
1074: * ```
1075: *
1076: * The above would result in a `/blog/` route being created, with both the
1077: * plugin & controller default parameters set.
1078: *
1079: * You can use `Router::plugin()` and `Router::prefix()` as shortcuts to creating
1080: * specific kinds of scopes.
1081: *
1082: * @param string $path The path prefix for the scope. This path will be prepended
1083: * to all routes connected in the scoped collection.
1084: * @param array|callable $params An array of routing defaults to add to each connected route.
1085: * If you have no parameters, this argument can be a callable.
1086: * @param callable|null $callback The callback to invoke with the scoped collection.
1087: * @throws \InvalidArgumentException When an invalid callable is provided.
1088: * @return void
1089: */
1090: public static function scope($path, $params = [], $callback = null)
1091: {
1092: $options = [];
1093: if (is_array($params)) {
1094: $options = $params;
1095: unset($params['routeClass'], $params['extensions']);
1096: }
1097: $builder = static::createRouteBuilder('/', $options);
1098: $builder->scope($path, $params, $callback);
1099: }
1100:
1101: /**
1102: * Create prefixed routes.
1103: *
1104: * This method creates a scoped route collection that includes
1105: * relevant prefix information.
1106: *
1107: * The path parameter is used to generate the routing parameter name.
1108: * For example a path of `admin` would result in `'prefix' => 'admin'` being
1109: * applied to all connected routes.
1110: *
1111: * The prefix name will be inflected to the underscore version to create
1112: * the routing path. If you want a custom path name, use the `path` option.
1113: *
1114: * You can re-open a prefix as many times as necessary, as well as nest prefixes.
1115: * Nested prefixes will result in prefix values like `admin/api` which translates
1116: * to the `Controller\Admin\Api\` namespace.
1117: *
1118: * @param string $name The prefix name to use.
1119: * @param array|callable $params An array of routing defaults to add to each connected route.
1120: * If you have no parameters, this argument can be a callable.
1121: * @param callable|null $callback The callback to invoke that builds the prefixed routes.
1122: * @return void
1123: */
1124: public static function prefix($name, $params = [], $callback = null)
1125: {
1126: if ($callback === null) {
1127: $callback = $params;
1128: $params = [];
1129: }
1130: $name = Inflector::underscore($name);
1131:
1132: if (empty($params['path'])) {
1133: $path = '/' . $name;
1134: } else {
1135: $path = $params['path'];
1136: unset($params['path']);
1137: }
1138:
1139: $params = array_merge($params, ['prefix' => $name]);
1140: static::scope($path, $params, $callback);
1141: }
1142:
1143: /**
1144: * Add plugin routes.
1145: *
1146: * This method creates a scoped route collection that includes
1147: * relevant plugin information.
1148: *
1149: * The plugin name will be inflected to the underscore version to create
1150: * the routing path. If you want a custom path name, use the `path` option.
1151: *
1152: * Routes connected in the scoped collection will have the correct path segment
1153: * prepended, and have a matching plugin routing key set.
1154: *
1155: * @param string $name The plugin name to build routes for
1156: * @param array|callable $options Either the options to use, or a callback
1157: * @param callable|null $callback The callback to invoke that builds the plugin routes.
1158: * Only required when $options is defined
1159: * @return void
1160: */
1161: public static function plugin($name, $options = [], $callback = null)
1162: {
1163: if ($callback === null) {
1164: $callback = $options;
1165: $options = [];
1166: }
1167: $params = ['plugin' => $name];
1168: if (empty($options['path'])) {
1169: $options['path'] = '/' . Inflector::underscore($name);
1170: }
1171: if (isset($options['_namePrefix'])) {
1172: $params['_namePrefix'] = $options['_namePrefix'];
1173: }
1174: static::scope($options['path'], $params, $callback);
1175: }
1176:
1177: /**
1178: * Get the route scopes and their connected routes.
1179: *
1180: * @return \Cake\Routing\Route\Route[]
1181: */
1182: public static function routes()
1183: {
1184: if (!static::$initialized) {
1185: static::_loadRoutes();
1186: }
1187:
1188: return static::$_collection->routes();
1189: }
1190:
1191: /**
1192: * Get the RouteCollection inside the Router
1193: *
1194: * @return \Cake\Routing\RouteCollection
1195: */
1196: public static function getRouteCollection()
1197: {
1198: return static::$_collection;
1199: }
1200:
1201: /**
1202: * Set the RouteCollection inside the Router
1203: *
1204: * @param RouteCollection $routeCollection route collection
1205: * @return void
1206: */
1207: public static function setRouteCollection($routeCollection)
1208: {
1209: static::$_collection = $routeCollection;
1210: static::$initialized = true;
1211: }
1212:
1213: /**
1214: * Loads route configuration
1215: *
1216: * @deprecated 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0
1217: * @return void
1218: */
1219: protected static function _loadRoutes()
1220: {
1221: static::$initialized = true;
1222: include CONFIG . 'routes.php';
1223: }
1224: }
1225: