CakeRoute.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * Redistributions of files must retain the above copyright notice.
  8. *
  9. * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  10. * @link http://cakephp.org CakePHP(tm) Project
  11. * @since CakePHP(tm) v 1.3
  12. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  13. */
  14. App::uses('Hash', 'Utility');
  15. /**
  16. * A single Route used by the Router to connect requests to
  17. * parameter maps.
  18. *
  19. * Not normally created as a standalone. Use Router::connect() to create
  20. * Routes for your application.
  21. *
  22. * @package Cake.Routing.Route
  23. */
  24. class CakeRoute {
  25. /**
  26. * An array of named segments in a Route.
  27. * `/:controller/:action/:id` has 3 key elements
  28. *
  29. * @var array
  30. */
  31. public $keys = array();
  32. /**
  33. * An array of additional parameters for the Route.
  34. *
  35. * @var array
  36. */
  37. public $options = array();
  38. /**
  39. * Default parameters for a Route
  40. *
  41. * @var array
  42. */
  43. public $defaults = array();
  44. /**
  45. * The routes template string.
  46. *
  47. * @var string
  48. */
  49. public $template = null;
  50. /**
  51. * Is this route a greedy route? Greedy routes have a `/*` in their
  52. * template
  53. *
  54. * @var string
  55. */
  56. protected $_greedy = false;
  57. /**
  58. * The compiled route regular expression
  59. *
  60. * @var string
  61. */
  62. protected $_compiledRoute = null;
  63. /**
  64. * HTTP header shortcut map. Used for evaluating header-based route expressions.
  65. *
  66. * @var array
  67. */
  68. protected $_headerMap = array(
  69. 'type' => 'content_type',
  70. 'method' => 'request_method',
  71. 'server' => 'server_name'
  72. );
  73. /**
  74. * Constructor for a Route
  75. *
  76. * @param string $template Template string with parameter placeholders
  77. * @param array $defaults Array of defaults for the route.
  78. * @param array $options Array of additional options for the Route
  79. */
  80. public function __construct($template, $defaults = array(), $options = array()) {
  81. $this->template = $template;
  82. $this->defaults = (array)$defaults;
  83. $this->options = (array)$options;
  84. }
  85. /**
  86. * Check if a Route has been compiled into a regular expression.
  87. *
  88. * @return boolean
  89. */
  90. public function compiled() {
  91. return !empty($this->_compiledRoute);
  92. }
  93. /**
  94. * Compiles the route's regular expression. Modifies defaults property so all necessary keys are set
  95. * and populates $this->names with the named routing elements.
  96. *
  97. * @return array Returns a string regular expression of the compiled route.
  98. */
  99. public function compile() {
  100. if ($this->compiled()) {
  101. return $this->_compiledRoute;
  102. }
  103. $this->_writeRoute();
  104. return $this->_compiledRoute;
  105. }
  106. /**
  107. * Builds a route regular expression. Uses the template, defaults and options
  108. * properties to compile a regular expression that can be used to parse request strings.
  109. *
  110. * @return void
  111. */
  112. protected function _writeRoute() {
  113. if (empty($this->template) || ($this->template === '/')) {
  114. $this->_compiledRoute = '#^/*$#';
  115. $this->keys = array();
  116. return;
  117. }
  118. $route = $this->template;
  119. $names = $routeParams = array();
  120. $parsed = preg_quote($this->template, '#');
  121. preg_match_all('#:([A-Za-z0-9_-]+[A-Z0-9a-z])#', $route, $namedElements);
  122. foreach ($namedElements[1] as $i => $name) {
  123. $search = '\\' . $namedElements[0][$i];
  124. if (isset($this->options[$name])) {
  125. $option = null;
  126. if ($name !== 'plugin' && array_key_exists($name, $this->defaults)) {
  127. $option = '?';
  128. }
  129. $slashParam = '/\\' . $namedElements[0][$i];
  130. if (strpos($parsed, $slashParam) !== false) {
  131. $routeParams[$slashParam] = '(?:/(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option;
  132. } else {
  133. $routeParams[$search] = '(?:(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option;
  134. }
  135. } else {
  136. $routeParams[$search] = '(?:(?P<' . $name . '>[^/]+))';
  137. }
  138. $names[] = $name;
  139. }
  140. if (preg_match('#\/\*\*$#', $route)) {
  141. $parsed = preg_replace('#/\\\\\*\\\\\*$#', '(?:/(?P<_trailing_>.*))?', $parsed);
  142. $this->_greedy = true;
  143. }
  144. if (preg_match('#\/\*$#', $route)) {
  145. $parsed = preg_replace('#/\\\\\*$#', '(?:/(?P<_args_>.*))?', $parsed);
  146. $this->_greedy = true;
  147. }
  148. krsort($routeParams);
  149. $parsed = str_replace(array_keys($routeParams), array_values($routeParams), $parsed);
  150. $this->_compiledRoute = '#^' . $parsed . '[/]*$#';
  151. $this->keys = $names;
  152. // Remove defaults that are also keys. They can cause match failures
  153. foreach ($this->keys as $key) {
  154. unset($this->defaults[$key]);
  155. }
  156. }
  157. /**
  158. * Checks to see if the given URL can be parsed by this route.
  159. * If the route can be parsed an array of parameters will be returned; if not
  160. * false will be returned. String urls are parsed if they match a routes regular expression.
  161. *
  162. * @param string $url The url to attempt to parse.
  163. * @return mixed Boolean false on failure, otherwise an array or parameters
  164. */
  165. public function parse($url) {
  166. if (!$this->compiled()) {
  167. $this->compile();
  168. }
  169. if (!preg_match($this->_compiledRoute, urldecode($url), $route)) {
  170. return false;
  171. }
  172. foreach ($this->defaults as $key => $val) {
  173. $key = (string)$key;
  174. if ($key[0] === '[' && preg_match('/^\[(\w+)\]$/', $key, $header)) {
  175. if (isset($this->_headerMap[$header[1]])) {
  176. $header = $this->_headerMap[$header[1]];
  177. } else {
  178. $header = 'http_' . $header[1];
  179. }
  180. $header = strtoupper($header);
  181. $val = (array)$val;
  182. $h = false;
  183. foreach ($val as $v) {
  184. if (env($header) === $v) {
  185. $h = true;
  186. }
  187. }
  188. if (!$h) {
  189. return false;
  190. }
  191. }
  192. }
  193. array_shift($route);
  194. $count = count($this->keys);
  195. for ($i = 0; $i <= $count; $i++) {
  196. unset($route[$i]);
  197. }
  198. $route['pass'] = $route['named'] = array();
  199. // Assign defaults, set passed args to pass
  200. foreach ($this->defaults as $key => $value) {
  201. if (isset($route[$key])) {
  202. continue;
  203. }
  204. if (is_int($key)) {
  205. $route['pass'][] = $value;
  206. continue;
  207. }
  208. $route[$key] = $value;
  209. }
  210. foreach ($this->keys as $key) {
  211. if (isset($route[$key])) {
  212. $route[$key] = rawurldecode($route[$key]);
  213. }
  214. }
  215. if (isset($route['_args_'])) {
  216. list($pass, $named) = $this->_parseArgs($route['_args_'], $route);
  217. $route['pass'] = array_merge($route['pass'], $pass);
  218. $route['named'] = $named;
  219. unset($route['_args_']);
  220. }
  221. if (isset($route['_trailing_'])) {
  222. $route['pass'][] = rawurldecode($route['_trailing_']);
  223. unset($route['_trailing_']);
  224. }
  225. // restructure 'pass' key route params
  226. if (isset($this->options['pass'])) {
  227. $j = count($this->options['pass']);
  228. while ($j--) {
  229. if (isset($route[$this->options['pass'][$j]])) {
  230. array_unshift($route['pass'], $route[$this->options['pass'][$j]]);
  231. }
  232. }
  233. }
  234. return $route;
  235. }
  236. /**
  237. * Parse passed and Named parameters into a list of passed args, and a hash of named parameters.
  238. * The local and global configuration for named parameters will be used.
  239. *
  240. * @param string $args A string with the passed & named params. eg. /1/page:2
  241. * @param string $context The current route context, which should contain controller/action keys.
  242. * @return array Array of ($pass, $named)
  243. */
  244. protected function _parseArgs($args, $context) {
  245. $pass = $named = array();
  246. $args = explode('/', $args);
  247. $namedConfig = Router::namedConfig();
  248. $greedy = $namedConfig['greedyNamed'];
  249. $rules = $namedConfig['rules'];
  250. if (!empty($this->options['named'])) {
  251. $greedy = isset($this->options['greedyNamed']) && $this->options['greedyNamed'] === true;
  252. foreach ((array)$this->options['named'] as $key => $val) {
  253. if (is_numeric($key)) {
  254. $rules[$val] = true;
  255. continue;
  256. }
  257. $rules[$key] = $val;
  258. }
  259. }
  260. foreach ($args as $param) {
  261. if (empty($param) && $param !== '0' && $param !== 0) {
  262. continue;
  263. }
  264. $separatorIsPresent = strpos($param, $namedConfig['separator']) !== false;
  265. if ((!isset($this->options['named']) || !empty($this->options['named'])) && $separatorIsPresent) {
  266. list($key, $val) = explode($namedConfig['separator'], $param, 2);
  267. $key = rawurldecode($key);
  268. $val = rawurldecode($val);
  269. $hasRule = isset($rules[$key]);
  270. $passIt = (!$hasRule && !$greedy) || ($hasRule && !$this->_matchNamed($val, $rules[$key], $context));
  271. if ($passIt) {
  272. $pass[] = rawurldecode($param);
  273. } else {
  274. if (preg_match_all('/\[([A-Za-z0-9_-]+)?\]/', $key, $matches, PREG_SET_ORDER)) {
  275. $matches = array_reverse($matches);
  276. $parts = explode('[', $key);
  277. $key = array_shift($parts);
  278. $arr = $val;
  279. foreach ($matches as $match) {
  280. if (empty($match[1])) {
  281. $arr = array($arr);
  282. } else {
  283. $arr = array(
  284. $match[1] => $arr
  285. );
  286. }
  287. }
  288. $val = $arr;
  289. }
  290. $named = array_merge_recursive($named, array($key => $val));
  291. }
  292. } else {
  293. $pass[] = rawurldecode($param);
  294. }
  295. }
  296. return array($pass, $named);
  297. }
  298. /**
  299. * Return true if a given named $param's $val matches a given $rule depending on $context. Currently implemented
  300. * rule types are controller, action and match that can be combined with each other.
  301. *
  302. * @param string $val The value of the named parameter
  303. * @param array $rule The rule(s) to apply, can also be a match string
  304. * @param string $context An array with additional context information (controller / action)
  305. * @return boolean
  306. */
  307. protected function _matchNamed($val, $rule, $context) {
  308. if ($rule === true || $rule === false) {
  309. return $rule;
  310. }
  311. if (is_string($rule)) {
  312. $rule = array('match' => $rule);
  313. }
  314. if (!is_array($rule)) {
  315. return false;
  316. }
  317. $controllerMatches = (
  318. !isset($rule['controller'], $context['controller']) ||
  319. in_array($context['controller'], (array)$rule['controller'])
  320. );
  321. if (!$controllerMatches) {
  322. return false;
  323. }
  324. $actionMatches = (
  325. !isset($rule['action'], $context['action']) ||
  326. in_array($context['action'], (array)$rule['action'])
  327. );
  328. if (!$actionMatches) {
  329. return false;
  330. }
  331. return (!isset($rule['match']) || preg_match('/' . $rule['match'] . '/', $val));
  332. }
  333. /**
  334. * Apply persistent parameters to a url array. Persistent parameters are a special
  335. * key used during route creation to force route parameters to persist when omitted from
  336. * a url array.
  337. *
  338. * @param array $url The array to apply persistent parameters to.
  339. * @param array $params An array of persistent values to replace persistent ones.
  340. * @return array An array with persistent parameters applied.
  341. */
  342. public function persistParams($url, $params) {
  343. foreach ($this->options['persist'] as $persistKey) {
  344. if (array_key_exists($persistKey, $params) && !isset($url[$persistKey])) {
  345. $url[$persistKey] = $params[$persistKey];
  346. }
  347. }
  348. return $url;
  349. }
  350. /**
  351. * Attempt to match a url array. If the url matches the route parameters and settings, then
  352. * return a generated string url. If the url doesn't match the route parameters, false will be returned.
  353. * This method handles the reverse routing or conversion of url arrays into string urls.
  354. *
  355. * @param array $url An array of parameters to check matching with.
  356. * @return mixed Either a string url for the parameters if they match or false.
  357. */
  358. public function match($url) {
  359. if (!$this->compiled()) {
  360. $this->compile();
  361. }
  362. $defaults = $this->defaults;
  363. if (isset($defaults['prefix'])) {
  364. $url['prefix'] = $defaults['prefix'];
  365. }
  366. //check that all the key names are in the url
  367. $keyNames = array_flip($this->keys);
  368. if (array_intersect_key($keyNames, $url) !== $keyNames) {
  369. return false;
  370. }
  371. // Missing defaults is a fail.
  372. if (array_diff_key($defaults, $url) !== array()) {
  373. return false;
  374. }
  375. $namedConfig = Router::namedConfig();
  376. $prefixes = Router::prefixes();
  377. $greedyNamed = $namedConfig['greedyNamed'];
  378. $allowedNamedParams = $namedConfig['rules'];
  379. $named = $pass = array();
  380. foreach ($url as $key => $value) {
  381. // keys that exist in the defaults and have different values is a match failure.
  382. $defaultExists = array_key_exists($key, $defaults);
  383. if ($defaultExists && $defaults[$key] != $value) {
  384. return false;
  385. } elseif ($defaultExists) {
  386. continue;
  387. }
  388. // If the key is a routed key, its not different yet.
  389. if (array_key_exists($key, $keyNames)) {
  390. continue;
  391. }
  392. // pull out passed args
  393. $numeric = is_numeric($key);
  394. if ($numeric && isset($defaults[$key]) && $defaults[$key] == $value) {
  395. continue;
  396. } elseif ($numeric) {
  397. $pass[] = $value;
  398. unset($url[$key]);
  399. continue;
  400. }
  401. // pull out named params if named params are greedy or a rule exists.
  402. if (
  403. ($greedyNamed || isset($allowedNamedParams[$key])) &&
  404. ($value !== false && $value !== null) &&
  405. (!in_array($key, $prefixes))
  406. ) {
  407. $named[$key] = $value;
  408. continue;
  409. }
  410. // keys that don't exist are different.
  411. if (!$defaultExists && !empty($value)) {
  412. return false;
  413. }
  414. }
  415. //if a not a greedy route, no extra params are allowed.
  416. if (!$this->_greedy && (!empty($pass) || !empty($named))) {
  417. return false;
  418. }
  419. //check patterns for routed params
  420. if (!empty($this->options)) {
  421. foreach ($this->options as $key => $pattern) {
  422. if (array_key_exists($key, $url) && !preg_match('#^' . $pattern . '$#', $url[$key])) {
  423. return false;
  424. }
  425. }
  426. }
  427. return $this->_writeUrl(array_merge($url, compact('pass', 'named')));
  428. }
  429. /**
  430. * Converts a matching route array into a url string. Composes the string url using the template
  431. * used to create the route.
  432. *
  433. * @param array $params The params to convert to a string url.
  434. * @return string Composed route string.
  435. */
  436. protected function _writeUrl($params) {
  437. if (isset($params['prefix'])) {
  438. $prefixed = $params['prefix'] . '_';
  439. }
  440. if (isset($prefixed, $params['action']) && strpos($params['action'], $prefixed) === 0) {
  441. $params['action'] = substr($params['action'], strlen($prefixed) * -1);
  442. unset($params['prefix']);
  443. }
  444. if (is_array($params['pass'])) {
  445. $params['pass'] = implode('/', array_map('rawurlencode', $params['pass']));
  446. }
  447. $namedConfig = Router::namedConfig();
  448. $separator = $namedConfig['separator'];
  449. if (!empty($params['named']) && is_array($params['named'])) {
  450. $named = array();
  451. foreach ($params['named'] as $key => $value) {
  452. if (is_array($value)) {
  453. $flat = Hash::flatten($value, '%5D%5B');
  454. foreach ($flat as $namedKey => $namedValue) {
  455. $named[] = $key . "%5B{$namedKey}%5D" . $separator . rawurlencode($namedValue);
  456. }
  457. } else {
  458. $named[] = $key . $separator . rawurlencode($value);
  459. }
  460. }
  461. $params['pass'] = $params['pass'] . '/' . implode('/', $named);
  462. }
  463. $out = $this->template;
  464. $search = $replace = array();
  465. foreach ($this->keys as $key) {
  466. $string = null;
  467. if (isset($params[$key])) {
  468. $string = $params[$key];
  469. } elseif (strpos($out, $key) != strlen($out) - strlen($key)) {
  470. $key .= '/';
  471. }
  472. $search[] = ':' . $key;
  473. $replace[] = $string;
  474. }
  475. $out = str_replace($search, $replace, $out);
  476. if (strpos($this->template, '*')) {
  477. $out = str_replace('*', $params['pass'], $out);
  478. }
  479. $out = str_replace('//', '/', $out);
  480. return $out;
  481. }
  482. }