Router.php 36 KB

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