Router.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185
  1. <?php
  2. /**
  3. * Parses the request URL into controller, action, and parameters.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.Routing
  16. * @since CakePHP(tm) v 0.2.9
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. App::uses('CakeRequest', 'Network');
  20. App::uses('CakeRoute', 'Routing/Route');
  21. /**
  22. * Parses the request URL into controller, action, and parameters. Uses the connected routes
  23. * to match the incoming url string to parameters that will allow the request to be dispatched. Also
  24. * handles converting parameter lists into url strings, using the connected routes. Routing allows you to decouple
  25. * the way the world interacts with your application (urls) and the implementation (controllers and actions).
  26. *
  27. * ### Connecting routes
  28. *
  29. * Connecting routes is done using Router::connect(). When parsing incoming requests or reverse matching
  30. * parameters, routes are enumerated in the order they were connected. You can modify the order of connected
  31. * routes using Router::promote(). For more information on routes and how to connect them see Router::connect().
  32. *
  33. * ### Named parameters
  34. *
  35. * Named parameters allow you to embed key:value pairs into path segments. This allows you create hash
  36. * structures using urls. You can define how named parameters work in your application using Router::connectNamed()
  37. *
  38. * @package Cake.Routing
  39. */
  40. class Router {
  41. /**
  42. * Array of routes connected with Router::connect()
  43. *
  44. * @var array
  45. */
  46. public static $routes = array();
  47. /**
  48. * Have routes been loaded
  49. *
  50. * @var boolean
  51. */
  52. public static $initialized = false;
  53. /**
  54. * List of action prefixes used in connected routes.
  55. * Includes admin prefix
  56. *
  57. * @var array
  58. */
  59. protected static $_prefixes = array();
  60. /**
  61. * Directive for Router to parse out file extensions for mapping to Content-types.
  62. *
  63. * @var boolean
  64. */
  65. protected static $_parseExtensions = false;
  66. /**
  67. * List of valid extensions to parse from a URL. If null, any extension is allowed.
  68. *
  69. * @var array
  70. */
  71. protected static $_validExtensions = array();
  72. /**
  73. * 'Constant' regular expression definitions for named route elements
  74. *
  75. */
  76. const ACTION = 'index|show|add|create|edit|update|remove|del|delete|view|item';
  77. const YEAR = '[12][0-9]{3}';
  78. const MONTH = '0[1-9]|1[012]';
  79. const DAY = '0[1-9]|[12][0-9]|3[01]';
  80. const ID = '[0-9]+';
  81. 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}';
  82. /**
  83. * Named expressions
  84. *
  85. * @var array
  86. */
  87. protected static $_namedExpressions = array(
  88. 'Action' => Router::ACTION,
  89. 'Year' => Router::YEAR,
  90. 'Month' => Router::MONTH,
  91. 'Day' => Router::DAY,
  92. 'ID' => Router::ID,
  93. 'UUID' => Router::UUID
  94. );
  95. /**
  96. * Stores all information necessary to decide what named arguments are parsed under what conditions.
  97. *
  98. * @var string
  99. */
  100. protected static $_namedConfig = array(
  101. 'default' => array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step'),
  102. 'greedyNamed' => true,
  103. 'separator' => ':',
  104. 'rules' => false,
  105. );
  106. /**
  107. * The route matching the URL of the current request
  108. *
  109. * @var array
  110. */
  111. protected static $_currentRoute = array();
  112. /**
  113. * Default HTTP request method => controller action map.
  114. *
  115. * @var array
  116. */
  117. protected static $_resourceMap = array(
  118. array('action' => 'index', 'method' => 'GET', 'id' => false),
  119. array('action' => 'view', 'method' => 'GET', 'id' => true),
  120. array('action' => 'add', 'method' => 'POST', 'id' => false),
  121. array('action' => 'edit', 'method' => 'PUT', 'id' => true),
  122. array('action' => 'delete', 'method' => 'DELETE', 'id' => true),
  123. array('action' => 'edit', 'method' => 'POST', 'id' => true)
  124. );
  125. /**
  126. * List of resource-mapped controllers
  127. *
  128. * @var array
  129. */
  130. protected static $_resourceMapped = array();
  131. /**
  132. * Maintains the request object stack for the current request.
  133. * This will contain more than one request object when requestAction is used.
  134. *
  135. * @var array
  136. */
  137. protected static $_requests = array();
  138. /**
  139. * Initial state is populated the first time reload() is called which is at the bottom
  140. * of this file. This is a cheat as get_class_vars() returns the value of static vars even if they
  141. * have changed.
  142. *
  143. * @var array
  144. */
  145. protected static $_initialState = array();
  146. /**
  147. * Default route class to use
  148. *
  149. * @var string
  150. */
  151. protected static $_routeClass = 'CakeRoute';
  152. /**
  153. * Set the default route class to use or return the current one
  154. *
  155. * @param string $routeClass to set as default
  156. * @return mixed void|string
  157. * @throws RouterException
  158. */
  159. public static function defaultRouteClass($routeClass = null) {
  160. if (is_null($routeClass)) {
  161. return self::$_routeClass;
  162. }
  163. self::$_routeClass = self::_validateRouteClass($routeClass);
  164. }
  165. /**
  166. * Validates that the passed route class exists and is a subclass of CakeRoute
  167. *
  168. * @param string $routeClass Route class name
  169. * @return string
  170. * @throws RouterException
  171. */
  172. protected static function _validateRouteClass($routeClass) {
  173. if (
  174. $routeClass != 'CakeRoute' &&
  175. (!class_exists($routeClass) || !is_subclass_of($routeClass, 'CakeRoute'))
  176. ) {
  177. throw new RouterException(__d('cake_dev', 'Route classes must extend CakeRoute'));
  178. }
  179. return $routeClass;
  180. }
  181. /**
  182. * Sets the Routing prefixes.
  183. *
  184. * @return void
  185. */
  186. protected static function _setPrefixes() {
  187. $routing = Configure::read('Routing');
  188. if (!empty($routing['prefixes'])) {
  189. self::$_prefixes = array_merge(self::$_prefixes, (array)$routing['prefixes']);
  190. }
  191. }
  192. /**
  193. * Gets the named route elements for use in app/Config/routes.php
  194. *
  195. * @return array Named route elements
  196. * @see Router::$_namedExpressions
  197. */
  198. public static function getNamedExpressions() {
  199. return self::$_namedExpressions;
  200. }
  201. /**
  202. * Resource map getter & setter.
  203. *
  204. * @param array $resourceMap Resource map
  205. * @return mixed
  206. * @see Router::$_resourceMap
  207. */
  208. public static function resourceMap($resourceMap = null) {
  209. if ($resourceMap === null) {
  210. return self::$_resourceMap;
  211. }
  212. self::$_resourceMap = $resourceMap;
  213. }
  214. /**
  215. * Connects a new Route in the router.
  216. *
  217. * Routes are a way of connecting request urls to objects in your application. At their core routes
  218. * are a set or regular expressions that are used to match requests to destinations.
  219. *
  220. * Examples:
  221. *
  222. * `Router::connect('/:controller/:action/*');`
  223. *
  224. * The first parameter will be used as a controller name while the second is used as the action name.
  225. * the '/*' syntax makes this route greedy in that it will match requests like `/posts/index` as well as requests
  226. * like `/posts/edit/1/foo/bar`.
  227. *
  228. * `Router::connect('/home-page', array('controller' => 'pages', 'action' => 'display', 'home'));`
  229. *
  230. * The above shows the use of route parameter defaults. And providing routing parameters for a static route.
  231. *
  232. * {{{
  233. * Router::connect(
  234. * '/:lang/:controller/:action/:id',
  235. * array(),
  236. * array('id' => '[0-9]+', 'lang' => '[a-z]{3}')
  237. * );
  238. * }}}
  239. *
  240. * Shows connecting a route with custom route parameters as well as providing patterns for those parameters.
  241. * Patterns for routing parameters do not need capturing groups, as one will be added for each route params.
  242. *
  243. * $options offers four 'special' keys. `pass`, `named`, `persist` and `routeClass`
  244. * have special meaning in the $options array.
  245. *
  246. * - `pass` is used to define which of the routed parameters should be shifted into the pass array. Adding a
  247. * parameter to pass will remove it from the regular route array. Ex. `'pass' => array('slug')`
  248. * - `persist` is used to define which route parameters should be automatically included when generating
  249. * new urls. You can override persistent parameters by redefining them in a url or remove them by
  250. * setting the parameter to `false`. Ex. `'persist' => array('lang')`
  251. * - `routeClass` is used to extend and change how individual routes parse requests and handle reverse routing,
  252. * via a custom routing class. Ex. `'routeClass' => 'SlugRoute'`
  253. * - `named` is used to configure named parameters at the route level. This key uses the same options
  254. * as Router::connectNamed()
  255. *
  256. * You can also add additional conditions for matching routes to the $defaults array.
  257. * The following conditions can be used:
  258. *
  259. * - `[type]` Only match requests for specific content types.
  260. * - `[method]` Only match requests with specific HTTP verbs.
  261. * - `[server]` Only match when $_SERVER['SERVER_NAME'] matches the given value.
  262. *
  263. * Example of using the `[method]` condition:
  264. *
  265. * `Router::connect('/tasks', array('controller' => 'tasks', 'action' => 'index', '[method]' => 'GET'));`
  266. *
  267. * The above route will only be matched for GET requests. POST requests will fail to match this route.
  268. *
  269. * @param string $route A string describing the template of the route
  270. * @param array $defaults An array describing the default route parameters. These parameters will be used by default
  271. * and can supply routing parameters that are not dynamic. See above.
  272. * @param array $options An array matching the named elements in the route to regular expressions which that
  273. * element should match. Also contains additional parameters such as which routed parameters should be
  274. * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a
  275. * custom routing class.
  276. * @see routes
  277. * @return array Array of routes
  278. * @throws RouterException
  279. */
  280. public static function connect($route, $defaults = array(), $options = array()) {
  281. self::$initialized = true;
  282. foreach (self::$_prefixes as $prefix) {
  283. if (isset($defaults[$prefix])) {
  284. if ($defaults[$prefix]) {
  285. $defaults['prefix'] = $prefix;
  286. } else {
  287. unset($defaults[$prefix]);
  288. }
  289. break;
  290. }
  291. }
  292. if (isset($defaults['prefix'])) {
  293. self::$_prefixes[] = $defaults['prefix'];
  294. self::$_prefixes = array_keys(array_flip(self::$_prefixes));
  295. }
  296. $defaults += array('plugin' => null);
  297. if (empty($options['action'])) {
  298. $defaults += array('action' => 'index');
  299. }
  300. $routeClass = self::$_routeClass;
  301. if (isset($options['routeClass'])) {
  302. if (strpos($options['routeClass'], '.') === false) {
  303. $routeClass = $options['routeClass'];
  304. } else {
  305. list(, $routeClass) = pluginSplit($options['routeClass'], true);
  306. }
  307. $routeClass = self::_validateRouteClass($routeClass);
  308. unset($options['routeClass']);
  309. }
  310. if ($routeClass == 'RedirectRoute' && isset($defaults['redirect'])) {
  311. $defaults = $defaults['redirect'];
  312. }
  313. self::$routes[] = new $routeClass($route, $defaults, $options);
  314. return self::$routes;
  315. }
  316. /**
  317. * Connects a new redirection Route in the router.
  318. *
  319. * Redirection routes are different from normal routes as they perform an actual
  320. * header redirection if a match is found. The redirection can occur within your
  321. * application or redirect to an outside location.
  322. *
  323. * Examples:
  324. *
  325. * `Router::redirect('/home/*', array('controller' => 'posts', 'action' => 'view', array('persist' => true)));`
  326. *
  327. * Redirects /home/* to /posts/view and passes the parameters to /posts/view. Using an array as the
  328. * redirect destination allows you to use other routes to define where a url string should be redirected to.
  329. *
  330. * `Router::redirect('/posts/*', 'http://google.com', array('status' => 302));`
  331. *
  332. * Redirects /posts/* to http://google.com with a HTTP status of 302
  333. *
  334. * ### Options:
  335. *
  336. * - `status` Sets the HTTP status (default 301)
  337. * - `persist` Passes the params to the redirected route, if it can. This is useful with greedy routes,
  338. * routes that end in `*` are greedy. As you can remap urls and not loose any passed/named args.
  339. *
  340. * @param string $route A string describing the template of the route
  341. * @param array $url A url to redirect to. Can be a string or a Cake array-based url
  342. * @param array $options An array matching the named elements in the route to regular expressions which that
  343. * element should match. Also contains additional parameters such as which routed parameters should be
  344. * shifted into the passed arguments. As well as supplying patterns for routing parameters.
  345. * @see routes
  346. * @return array Array of routes
  347. */
  348. public static function redirect($route, $url, $options = array()) {
  349. App::uses('RedirectRoute', 'Routing/Route');
  350. $options['routeClass'] = 'RedirectRoute';
  351. if (is_string($url)) {
  352. $url = array('redirect' => $url);
  353. }
  354. return self::connect($route, $url, $options);
  355. }
  356. /**
  357. * Specifies what named parameters CakePHP should be parsing out of incoming urls. By default
  358. * CakePHP will parse every named parameter out of incoming URLs. However, if you want to take more
  359. * control over how named parameters are parsed you can use one of the following setups:
  360. *
  361. * Do not parse any named parameters:
  362. *
  363. * {{{ Router::connectNamed(false); }}}
  364. *
  365. * Parse only default parameters used for CakePHP's pagination:
  366. *
  367. * {{{ Router::connectNamed(false, array('default' => true)); }}}
  368. *
  369. * Parse only the page parameter if its value is a number:
  370. *
  371. * {{{ Router::connectNamed(array('page' => '[\d]+'), array('default' => false, 'greedy' => false)); }}}
  372. *
  373. * Parse only the page parameter no matter what.
  374. *
  375. * {{{ Router::connectNamed(array('page'), array('default' => false, 'greedy' => false)); }}}
  376. *
  377. * Parse only the page parameter if the current action is 'index'.
  378. *
  379. * {{{
  380. * Router::connectNamed(
  381. * array('page' => array('action' => 'index')),
  382. * array('default' => false, 'greedy' => false)
  383. * );
  384. * }}}
  385. *
  386. * Parse only the page parameter if the current action is 'index' and the controller is 'pages'.
  387. *
  388. * {{{
  389. * Router::connectNamed(
  390. * array('page' => array('action' => 'index', 'controller' => 'pages')),
  391. * array('default' => false, 'greedy' => false)
  392. * );
  393. * }}}
  394. *
  395. * ### Options
  396. *
  397. * - `greedy` Setting this to true will make Router parse all named params. Setting it to false will
  398. * parse only the connected named params.
  399. * - `default` Set this to true to merge in the default set of named parameters.
  400. * - `reset` Set to true to clear existing rules and start fresh.
  401. * - `separator` Change the string used to separate the key & value in a named parameter. Defaults to `:`
  402. *
  403. * @param array $named A list of named parameters. Key value pairs are accepted where values are
  404. * either regex strings to match, or arrays as seen above.
  405. * @param array $options Allows to control all settings: separator, greedy, reset, default
  406. * @return array
  407. */
  408. public static function connectNamed($named, $options = array()) {
  409. if (isset($options['separator'])) {
  410. self::$_namedConfig['separator'] = $options['separator'];
  411. unset($options['separator']);
  412. }
  413. if ($named === true || $named === false) {
  414. $options = array_merge(array('default' => $named, 'reset' => true, 'greedy' => $named), $options);
  415. $named = array();
  416. } else {
  417. $options = array_merge(array('default' => false, 'reset' => false, 'greedy' => true), $options);
  418. }
  419. if ($options['reset'] || self::$_namedConfig['rules'] === false) {
  420. self::$_namedConfig['rules'] = array();
  421. }
  422. if ($options['default']) {
  423. $named = array_merge($named, self::$_namedConfig['default']);
  424. }
  425. foreach ($named as $key => $val) {
  426. if (is_numeric($key)) {
  427. self::$_namedConfig['rules'][$val] = true;
  428. } else {
  429. self::$_namedConfig['rules'][$key] = $val;
  430. }
  431. }
  432. self::$_namedConfig['greedyNamed'] = $options['greedy'];
  433. return self::$_namedConfig;
  434. }
  435. /**
  436. * Gets the current named parameter configuration values.
  437. *
  438. * @return array
  439. * @see Router::$_namedConfig
  440. */
  441. public static function namedConfig() {
  442. return self::$_namedConfig;
  443. }
  444. /**
  445. * Creates REST resource routes for the given controller(s). When creating resource routes
  446. * for a plugin, by default the prefix will be changed to the lower_underscore version of the plugin
  447. * name. By providing a prefix you can override this behavior.
  448. *
  449. * ### Options:
  450. *
  451. * - 'id' - The regular expression fragment to use when matching IDs. By default, matches
  452. * integer values and UUIDs.
  453. * - 'prefix' - URL prefix to use for the generated routes. Defaults to '/'.
  454. *
  455. * @param string|array $controller A controller name or array of controller names (i.e. "Posts" or "ListItems")
  456. * @param array $options Options to use when generating REST routes
  457. * @return array Array of mapped resources
  458. */
  459. public static function mapResources($controller, $options = array()) {
  460. $hasPrefix = isset($options['prefix']);
  461. $options = array_merge(array(
  462. 'prefix' => '/',
  463. 'id' => self::ID . '|' . self::UUID
  464. ), $options);
  465. $prefix = $options['prefix'];
  466. foreach ((array)$controller as $name) {
  467. list($plugin, $name) = pluginSplit($name);
  468. $urlName = Inflector::underscore($name);
  469. $plugin = Inflector::underscore($plugin);
  470. if ($plugin && !$hasPrefix) {
  471. $prefix = '/' . $plugin . '/';
  472. }
  473. foreach (self::$_resourceMap as $params) {
  474. $url = $prefix . $urlName . (($params['id']) ? '/:id' : '');
  475. Router::connect($url,
  476. array(
  477. 'plugin' => $plugin,
  478. 'controller' => $urlName,
  479. 'action' => $params['action'],
  480. '[method]' => $params['method']
  481. ),
  482. array('id' => $options['id'], 'pass' => array('id'))
  483. );
  484. }
  485. self::$_resourceMapped[] = $urlName;
  486. }
  487. return self::$_resourceMapped;
  488. }
  489. /**
  490. * Returns the list of prefixes used in connected routes
  491. *
  492. * @return array A list of prefixes used in connected routes
  493. */
  494. public static function prefixes() {
  495. return self::$_prefixes;
  496. }
  497. /**
  498. * Parses given URL string. Returns 'routing' parameters for that url.
  499. *
  500. * @param string $url URL to be parsed
  501. * @return array Parsed elements from URL
  502. */
  503. public static function parse($url) {
  504. if (!self::$initialized) {
  505. self::_loadRoutes();
  506. }
  507. $ext = null;
  508. $out = array();
  509. if (strlen($url) && strpos($url, '/') !== 0) {
  510. $url = '/' . $url;
  511. }
  512. if (strpos($url, '?') !== false) {
  513. $url = substr($url, 0, strpos($url, '?'));
  514. }
  515. extract(self::_parseExtension($url));
  516. for ($i = 0, $len = count(self::$routes); $i < $len; $i++) {
  517. $route =& self::$routes[$i];
  518. if (($r = $route->parse($url)) !== false) {
  519. self::$_currentRoute[] =& $route;
  520. $out = $r;
  521. break;
  522. }
  523. }
  524. if (isset($out['prefix'])) {
  525. $out['action'] = $out['prefix'] . '_' . $out['action'];
  526. }
  527. if (!empty($ext) && !isset($out['ext'])) {
  528. $out['ext'] = $ext;
  529. }
  530. return $out;
  531. }
  532. /**
  533. * Parses a file extension out of a URL, if Router::parseExtensions() is enabled.
  534. *
  535. * @param string $url
  536. * @return array Returns an array containing the altered URL and the parsed extension.
  537. */
  538. protected static function _parseExtension($url) {
  539. $ext = null;
  540. if (self::$_parseExtensions) {
  541. if (preg_match('/\.[0-9a-zA-Z]*$/', $url, $match) === 1) {
  542. $match = substr($match[0], 1);
  543. if (empty(self::$_validExtensions)) {
  544. $url = substr($url, 0, strpos($url, '.' . $match));
  545. $ext = $match;
  546. } else {
  547. foreach (self::$_validExtensions as $name) {
  548. if (strcasecmp($name, $match) === 0) {
  549. $url = substr($url, 0, strpos($url, '.' . $name));
  550. $ext = $match;
  551. break;
  552. }
  553. }
  554. }
  555. }
  556. }
  557. return compact('ext', 'url');
  558. }
  559. /**
  560. * Takes parameter and path information back from the Dispatcher, sets these
  561. * parameters as the current request parameters that are merged with url arrays
  562. * created later in the request.
  563. *
  564. * Nested requests will create a stack of requests. You can remove requests using
  565. * Router::popRequest(). This is done automatically when using Object::requestAction().
  566. *
  567. * Will accept either a CakeRequest object or an array of arrays. Support for
  568. * accepting arrays may be removed in the future.
  569. *
  570. * @param CakeRequest|array $request Parameters and path information or a CakeRequest object.
  571. * @return void
  572. */
  573. public static function setRequestInfo($request) {
  574. if ($request instanceof CakeRequest) {
  575. self::$_requests[] = $request;
  576. } else {
  577. $requestObj = new CakeRequest();
  578. $request += array(array(), array());
  579. $request[0] += array('controller' => false, 'action' => false, 'plugin' => null);
  580. $requestObj->addParams($request[0])->addPaths($request[1]);
  581. self::$_requests[] = $requestObj;
  582. }
  583. }
  584. /**
  585. * Pops a request off of the request stack. Used when doing requestAction
  586. *
  587. * @return CakeRequest The request removed from the stack.
  588. * @see Router::setRequestInfo()
  589. * @see Object::requestAction()
  590. */
  591. public static function popRequest() {
  592. return array_pop(self::$_requests);
  593. }
  594. /**
  595. * Get the either the current request object, or the first one.
  596. *
  597. * @param boolean $current Whether you want the request from the top of the stack or the first one.
  598. * @return CakeRequest or null.
  599. */
  600. public static function getRequest($current = false) {
  601. if ($current) {
  602. $i = count(self::$_requests) - 1;
  603. return isset(self::$_requests[$i]) ? self::$_requests[$i] : null;
  604. }
  605. return isset(self::$_requests[0]) ? self::$_requests[0] : null;
  606. }
  607. /**
  608. * Gets parameter information
  609. *
  610. * @param boolean $current Get current request parameter, useful when using requestAction
  611. * @return array Parameter information
  612. */
  613. public static function getParams($current = false) {
  614. if ($current && self::$_requests) {
  615. return self::$_requests[count(self::$_requests) - 1]->params;
  616. }
  617. if (isset(self::$_requests[0])) {
  618. return self::$_requests[0]->params;
  619. }
  620. return array();
  621. }
  622. /**
  623. * Gets URL parameter by name
  624. *
  625. * @param string $name Parameter name
  626. * @param boolean $current Current parameter, useful when using requestAction
  627. * @return string Parameter value
  628. */
  629. public static function getParam($name = 'controller', $current = false) {
  630. $params = Router::getParams($current);
  631. if (isset($params[$name])) {
  632. return $params[$name];
  633. }
  634. return null;
  635. }
  636. /**
  637. * Gets path information
  638. *
  639. * @param boolean $current Current parameter, useful when using requestAction
  640. * @return array
  641. */
  642. public static function getPaths($current = false) {
  643. if ($current) {
  644. return self::$_requests[count(self::$_requests) - 1];
  645. }
  646. if (!isset(self::$_requests[0])) {
  647. return array('base' => null);
  648. }
  649. return array('base' => self::$_requests[0]->base);
  650. }
  651. /**
  652. * Reloads default Router settings. Resets all class variables and
  653. * removes all connected routes.
  654. *
  655. * @return void
  656. */
  657. public static function reload() {
  658. if (empty(self::$_initialState)) {
  659. self::$_initialState = get_class_vars('Router');
  660. self::_setPrefixes();
  661. return;
  662. }
  663. foreach (self::$_initialState as $key => $val) {
  664. if ($key != '_initialState') {
  665. self::${$key} = $val;
  666. }
  667. }
  668. self::_setPrefixes();
  669. }
  670. /**
  671. * Promote a route (by default, the last one added) to the beginning of the list
  672. *
  673. * @param integer $which A zero-based array index representing the route to move. For example,
  674. * if 3 routes have been added, the last route would be 2.
  675. * @return boolean Returns false if no route exists at the position specified by $which.
  676. */
  677. public static function promote($which = null) {
  678. if ($which === null) {
  679. $which = count(self::$routes) - 1;
  680. }
  681. if (!isset(self::$routes[$which])) {
  682. return false;
  683. }
  684. $route =& self::$routes[$which];
  685. unset(self::$routes[$which]);
  686. array_unshift(self::$routes, $route);
  687. return true;
  688. }
  689. /**
  690. * Finds URL for specified action.
  691. *
  692. * Returns an URL pointing to a combination of controller and action. Param
  693. * $url can be:
  694. *
  695. * - Empty - the method will find address to actual controller/action.
  696. * - '/' - the method will find base URL of application.
  697. * - A combination of controller/action - the method will find url for it.
  698. *
  699. * There are a few 'special' parameters that can change the final URL string that is generated
  700. *
  701. * - `base` - Set to false to remove the base path from the generated url. If your application
  702. * is not in the root directory, this can be used to generate urls that are 'cake relative'.
  703. * cake relative urls are required when using requestAction.
  704. * - `?` - Takes an array of query string parameters
  705. * - `#` - Allows you to set url hash fragments.
  706. * - `full_base` - If true the `FULL_BASE_URL` constant will be prepended to generated urls.
  707. *
  708. * @param string|array $url Cake-relative URL, like "/products/edit/92" or "/presidents/elect/4"
  709. * or an array specifying any of the following: 'controller', 'action',
  710. * and/or 'plugin', in addition to named arguments (keyed array elements),
  711. * and standard URL arguments (indexed array elements)
  712. * @param bool|array $full If (bool) true, the full base URL will be prepended to the result.
  713. * If an array accepts the following keys
  714. * - escape - used when making urls embedded in html escapes query string '&'
  715. * - full - if true the full base URL will be prepended.
  716. * @return string Full translated URL with base path.
  717. */
  718. public static function url($url = null, $full = false) {
  719. if (!self::$initialized) {
  720. self::_loadRoutes();
  721. }
  722. $params = array('plugin' => null, 'controller' => null, 'action' => 'index');
  723. if (is_bool($full)) {
  724. $escape = false;
  725. } else {
  726. extract($full + array('escape' => false, 'full' => false));
  727. }
  728. $path = array('base' => null);
  729. if (!empty(self::$_requests)) {
  730. $request = self::$_requests[count(self::$_requests) - 1];
  731. $params = $request->params;
  732. $path = array('base' => $request->base, 'here' => $request->here);
  733. }
  734. $base = $path['base'];
  735. $extension = $output = $q = $frag = null;
  736. if (empty($url)) {
  737. $output = isset($path['here']) ? $path['here'] : '/';
  738. if ($full && defined('FULL_BASE_URL')) {
  739. $output = FULL_BASE_URL . $output;
  740. }
  741. return $output;
  742. } elseif (is_array($url)) {
  743. if (isset($url['base']) && $url['base'] === false) {
  744. $base = null;
  745. unset($url['base']);
  746. }
  747. if (isset($url['full_base']) && $url['full_base'] === true) {
  748. $full = true;
  749. unset($url['full_base']);
  750. }
  751. if (isset($url['?'])) {
  752. $q = $url['?'];
  753. unset($url['?']);
  754. }
  755. if (isset($url['#'])) {
  756. $frag = '#' . $url['#'];
  757. unset($url['#']);
  758. }
  759. if (isset($url['ext'])) {
  760. $extension = '.' . $url['ext'];
  761. unset($url['ext']);
  762. }
  763. if (empty($url['action'])) {
  764. if (empty($url['controller']) || $params['controller'] === $url['controller']) {
  765. $url['action'] = $params['action'];
  766. } else {
  767. $url['action'] = 'index';
  768. }
  769. }
  770. $prefixExists = (array_intersect_key($url, array_flip(self::$_prefixes)));
  771. foreach (self::$_prefixes as $prefix) {
  772. if (!empty($params[$prefix]) && !$prefixExists) {
  773. $url[$prefix] = true;
  774. } elseif (isset($url[$prefix]) && !$url[$prefix]) {
  775. unset($url[$prefix]);
  776. }
  777. if (isset($url[$prefix]) && strpos($url['action'], $prefix . '_') === 0) {
  778. $url['action'] = substr($url['action'], strlen($prefix) + 1);
  779. }
  780. }
  781. $url += array('controller' => $params['controller'], 'plugin' => $params['plugin']);
  782. $match = false;
  783. for ($i = 0, $len = count(self::$routes); $i < $len; $i++) {
  784. $originalUrl = $url;
  785. if (isset(self::$routes[$i]->options['persist'], $params)) {
  786. $url = self::$routes[$i]->persistParams($url, $params);
  787. }
  788. if ($match = self::$routes[$i]->match($url)) {
  789. $output = trim($match, '/');
  790. break;
  791. }
  792. $url = $originalUrl;
  793. }
  794. if ($match === false) {
  795. $output = self::_handleNoRoute($url);
  796. }
  797. } else {
  798. if (preg_match('/:\/\/|^(javascript|mailto|tel|sms):|^\#/i', $url)) {
  799. return $url;
  800. }
  801. if (substr($url, 0, 1) === '/') {
  802. $output = substr($url, 1);
  803. } else {
  804. foreach (self::$_prefixes as $prefix) {
  805. if (isset($params[$prefix])) {
  806. $output .= $prefix . '/';
  807. break;
  808. }
  809. }
  810. if (!empty($params['plugin']) && $params['plugin'] !== $params['controller']) {
  811. $output .= Inflector::underscore($params['plugin']) . '/';
  812. }
  813. $output .= Inflector::underscore($params['controller']) . '/' . $url;
  814. }
  815. }
  816. $protocol = preg_match('#^[a-z][a-z0-9+-.]*\://#i', $output);
  817. if ($protocol === 0) {
  818. $output = str_replace('//', '/', $base . '/' . $output);
  819. if ($full && defined('FULL_BASE_URL')) {
  820. $output = FULL_BASE_URL . $output;
  821. }
  822. if (!empty($extension)) {
  823. $output = rtrim($output, '/');
  824. }
  825. }
  826. return $output . $extension . self::queryString($q, array(), $escape) . $frag;
  827. }
  828. /**
  829. * A special fallback method that handles url arrays that cannot match
  830. * any defined routes.
  831. *
  832. * @param array $url A url that didn't match any routes
  833. * @return string A generated url for the array
  834. * @see Router::url()
  835. */
  836. protected static function _handleNoRoute($url) {
  837. $named = $args = array();
  838. $skip = array_merge(
  839. array('bare', 'action', 'controller', 'plugin', 'prefix'),
  840. self::$_prefixes
  841. );
  842. $keys = array_values(array_diff(array_keys($url), $skip));
  843. $count = count($keys);
  844. // Remove this once parsed URL parameters can be inserted into 'pass'
  845. for ($i = 0; $i < $count; $i++) {
  846. $key = $keys[$i];
  847. if (is_numeric($keys[$i])) {
  848. $args[] = $url[$key];
  849. } else {
  850. $named[$key] = $url[$key];
  851. }
  852. }
  853. list($args, $named) = array(Hash::filter($args), Hash::filter($named));
  854. foreach (self::$_prefixes as $prefix) {
  855. $prefixed = $prefix . '_';
  856. if (!empty($url[$prefix]) && strpos($url['action'], $prefixed) === 0) {
  857. $url['action'] = substr($url['action'], strlen($prefixed) * -1);
  858. break;
  859. }
  860. }
  861. if (empty($named) && empty($args) && (!isset($url['action']) || $url['action'] === 'index')) {
  862. $url['action'] = null;
  863. }
  864. $urlOut = array_filter(array($url['controller'], $url['action']));
  865. if (isset($url['plugin'])) {
  866. array_unshift($urlOut, $url['plugin']);
  867. }
  868. foreach (self::$_prefixes as $prefix) {
  869. if (isset($url[$prefix])) {
  870. array_unshift($urlOut, $prefix);
  871. break;
  872. }
  873. }
  874. $output = implode('/', $urlOut);
  875. if (!empty($args)) {
  876. $output .= '/' . implode('/', array_map('rawurlencode', $args));
  877. }
  878. if (!empty($named)) {
  879. foreach ($named as $name => $value) {
  880. if (is_array($value)) {
  881. $flattend = Hash::flatten($value, '%5D%5B');
  882. foreach ($flattend as $namedKey => $namedValue) {
  883. $output .= '/' . $name . "%5B{$namedKey}%5D" . self::$_namedConfig['separator'] . rawurlencode($namedValue);
  884. }
  885. } else {
  886. $output .= '/' . $name . self::$_namedConfig['separator'] . rawurlencode($value);
  887. }
  888. }
  889. }
  890. return $output;
  891. }
  892. /**
  893. * Generates a well-formed querystring from $q
  894. *
  895. * @param string|array $q Query string Either a string of already compiled query string arguments or
  896. * an array of arguments to convert into a query string.
  897. * @param array $extra Extra querystring parameters.
  898. * @param boolean $escape Whether or not to use escaped &
  899. * @return array
  900. */
  901. public static function queryString($q, $extra = array(), $escape = false) {
  902. if (empty($q) && empty($extra)) {
  903. return null;
  904. }
  905. $join = '&';
  906. if ($escape === true) {
  907. $join = '&amp;';
  908. }
  909. $out = '';
  910. if (is_array($q)) {
  911. $q = array_merge($q, $extra);
  912. } else {
  913. $out = $q;
  914. $q = $extra;
  915. }
  916. $addition = http_build_query($q, null, $join);
  917. if ($out && $addition && substr($out, strlen($join) * -1, strlen($join)) != $join) {
  918. $out .= $join;
  919. }
  920. $out .= $addition;
  921. if (isset($out[0]) && $out[0] != '?') {
  922. $out = '?' . $out;
  923. }
  924. return $out;
  925. }
  926. /**
  927. * Reverses a parsed parameter array into a string. Works similarly to Router::url(), but
  928. * Since parsed URL's contain additional 'pass' and 'named' as well as 'url.url' keys.
  929. * Those keys need to be specially handled in order to reverse a params array into a string url.
  930. *
  931. * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those
  932. * are used for CakePHP internals and should not normally be part of an output url.
  933. *
  934. * @param CakeRequest|array $params The params array or CakeRequest object that needs to be reversed.
  935. * @param boolean $full Set to true to include the full url including the protocol when reversing
  936. * the url.
  937. * @return string The string that is the reversed result of the array
  938. */
  939. public static function reverse($params, $full = false) {
  940. if ($params instanceof CakeRequest) {
  941. $url = $params->query;
  942. $params = $params->params;
  943. } else {
  944. $url = $params['url'];
  945. }
  946. $pass = isset($params['pass']) ? $params['pass'] : array();
  947. $named = isset($params['named']) ? $params['named'] : array();
  948. unset(
  949. $params['pass'], $params['named'], $params['paging'], $params['models'], $params['url'], $url['url'],
  950. $params['autoRender'], $params['bare'], $params['requested'], $params['return'],
  951. $params['_Token']
  952. );
  953. $params = array_merge($params, $pass, $named);
  954. if (!empty($url)) {
  955. $params['?'] = $url;
  956. }
  957. return Router::url($params, $full);
  958. }
  959. /**
  960. * Normalizes a URL for purposes of comparison. Will strip the base path off
  961. * and replace any double /'s. It will not unify the casing and underscoring
  962. * of the input value.
  963. *
  964. * @param array|string $url URL to normalize Either an array or a string url.
  965. * @return string Normalized URL
  966. */
  967. public static function normalize($url = '/') {
  968. if (is_array($url)) {
  969. $url = Router::url($url);
  970. }
  971. if (preg_match('/^[a-z\-]+:\/\//', $url)) {
  972. return $url;
  973. }
  974. $request = Router::getRequest();
  975. if (!empty($request->base) && stristr($url, $request->base)) {
  976. $url = preg_replace('/^' . preg_quote($request->base, '/') . '/', '', $url, 1);
  977. }
  978. $url = '/' . $url;
  979. while (strpos($url, '//') !== false) {
  980. $url = str_replace('//', '/', $url);
  981. }
  982. $url = preg_replace('/(?:(\/$))/', '', $url);
  983. if (empty($url)) {
  984. return '/';
  985. }
  986. return $url;
  987. }
  988. /**
  989. * Returns the route matching the current request URL.
  990. *
  991. * @return CakeRoute Matching route object.
  992. */
  993. public static function &requestRoute() {
  994. return self::$_currentRoute[0];
  995. }
  996. /**
  997. * Returns the route matching the current request (useful for requestAction traces)
  998. *
  999. * @return CakeRoute Matching route object.
  1000. */
  1001. public static function &currentRoute() {
  1002. return self::$_currentRoute[count(self::$_currentRoute) - 1];
  1003. }
  1004. /**
  1005. * Removes the plugin name from the base URL.
  1006. *
  1007. * @param string $base Base URL
  1008. * @param string $plugin Plugin name
  1009. * @return string base url with plugin name removed if present
  1010. */
  1011. public static function stripPlugin($base, $plugin = null) {
  1012. if ($plugin) {
  1013. $base = preg_replace('/(?:' . $plugin . ')/', '', $base);
  1014. $base = str_replace('//', '', $base);
  1015. $pos1 = strrpos($base, '/');
  1016. $char = strlen($base) - 1;
  1017. if ($pos1 === $char) {
  1018. $base = substr($base, 0, $char);
  1019. }
  1020. }
  1021. return $base;
  1022. }
  1023. /**
  1024. * Instructs the router to parse out file extensions from the URL. For example,
  1025. * http://example.com/posts.rss would yield an file extension of "rss".
  1026. * The file extension itself is made available in the controller as
  1027. * `$this->params['ext']`, and is used by the RequestHandler component to
  1028. * automatically switch to alternate layouts and templates, and load helpers
  1029. * corresponding to the given content, i.e. RssHelper. Switching layouts and helpers
  1030. * requires that the chosen extension has a defined mime type in `CakeResponse`
  1031. *
  1032. * A list of valid extension can be passed to this method, i.e. Router::parseExtensions('rss', 'xml');
  1033. * If no parameters are given, anything after the first . (dot) after the last / in the URL will be
  1034. * parsed, excluding querystring parameters (i.e. ?q=...).
  1035. *
  1036. * @return void
  1037. * @see RequestHandler::startup()
  1038. */
  1039. public static function parseExtensions() {
  1040. self::$_parseExtensions = true;
  1041. if (func_num_args() > 0) {
  1042. self::setExtensions(func_get_args(), false);
  1043. }
  1044. }
  1045. /**
  1046. * Get the list of extensions that can be parsed by Router.
  1047. * To initially set extensions use `Router::parseExtensions()`
  1048. * To add more see `setExtensions()`
  1049. *
  1050. * @return array Array of extensions Router is configured to parse.
  1051. */
  1052. public static function extensions() {
  1053. if (!self::$initialized) {
  1054. self::_loadRoutes();
  1055. }
  1056. return self::$_validExtensions;
  1057. }
  1058. /**
  1059. * Set/add valid extensions.
  1060. * To have the extensions parsed you still need to call `Router::parseExtensions()`
  1061. *
  1062. * @param array $extensions List of extensions to be added as valid extension
  1063. * @param boolean $merge Default true will merge extensions. Set to false to override current extensions
  1064. * @return array
  1065. */
  1066. public static function setExtensions($extensions, $merge = true) {
  1067. if (!is_array($extensions)) {
  1068. return self::$_validExtensions;
  1069. }
  1070. if (!$merge) {
  1071. return self::$_validExtensions = $extensions;
  1072. }
  1073. return self::$_validExtensions = array_merge(self::$_validExtensions, $extensions);
  1074. }
  1075. /**
  1076. * Loads route configuration
  1077. *
  1078. * @return void
  1079. */
  1080. protected static function _loadRoutes() {
  1081. self::$initialized = true;
  1082. include APP . 'Config' . DS . 'routes.php';
  1083. }
  1084. }
  1085. //Save the initial state
  1086. Router::reload();