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