PageRenderTime 31ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Cake/Routing/Route/CakeRoute.php

http://github.com/cakephp/cakephp
PHP | 513 lines | 321 code | 49 blank | 143 comment | 81 complexity | 6177b011000cbfb9117053b5954d556d MD5 | raw file
Possible License(s): JSON
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright 2005-2011, 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-2011, 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('Set', '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<_args_>.*))?', $parsed);
  142. $this->_greedy = true;
  143. }
  144. krsort($routeParams);
  145. $parsed = str_replace(array_keys($routeParams), array_values($routeParams), $parsed);
  146. $this->_compiledRoute = '#^' . $parsed . '[/]*$#';
  147. $this->keys = $names;
  148. //remove defaults that are also keys. They can cause match failures
  149. foreach ($this->keys as $key) {
  150. unset($this->defaults[$key]);
  151. }
  152. }
  153. /**
  154. * Checks to see if the given URL can be parsed by this route.
  155. * If the route can be parsed an array of parameters will be returned; if not
  156. * false will be returned. String urls are parsed if they match a routes regular expression.
  157. *
  158. * @param string $url The url to attempt to parse.
  159. * @return mixed Boolean false on failure, otherwise an array or parameters
  160. */
  161. public function parse($url) {
  162. if (!$this->compiled()) {
  163. $this->compile();
  164. }
  165. if (!preg_match($this->_compiledRoute, $url, $route)) {
  166. return false;
  167. }
  168. foreach ($this->defaults as $key => $val) {
  169. if ($key[0] === '[' && preg_match('/^\[(\w+)\]$/', $key, $header)) {
  170. if (isset($this->_headerMap[$header[1]])) {
  171. $header = $this->_headerMap[$header[1]];
  172. } else {
  173. $header = 'http_' . $header[1];
  174. }
  175. $header = strtoupper($header);
  176. $val = (array)$val;
  177. $h = false;
  178. foreach ($val as $v) {
  179. if (env($header) === $v) {
  180. $h = true;
  181. }
  182. }
  183. if (!$h) {
  184. return false;
  185. }
  186. }
  187. }
  188. array_shift($route);
  189. $count = count($this->keys);
  190. for ($i = 0; $i <= $count; $i++) {
  191. unset($route[$i]);
  192. }
  193. $route['pass'] = $route['named'] = array();
  194. // Assign defaults, set passed args to pass
  195. foreach ($this->defaults as $key => $value) {
  196. if (isset($route[$key])) {
  197. continue;
  198. }
  199. if (is_integer($key)) {
  200. $route['pass'][] = $value;
  201. continue;
  202. }
  203. $route[$key] = $value;
  204. }
  205. if (isset($route['_args_'])) {
  206. list($pass, $named) = $this->_parseArgs($route['_args_'], $route);
  207. $route['pass'] = array_merge($route['pass'], $pass);
  208. $route['named'] = $named;
  209. unset($route['_args_']);
  210. }
  211. // restructure 'pass' key route params
  212. if (isset($this->options['pass'])) {
  213. $j = count($this->options['pass']);
  214. while ($j--) {
  215. if (isset($route[$this->options['pass'][$j]])) {
  216. array_unshift($route['pass'], $route[$this->options['pass'][$j]]);
  217. }
  218. }
  219. }
  220. return $route;
  221. }
  222. /**
  223. * Parse passed and Named parameters into a list of passed args, and a hash of named parameters.
  224. * The local and global configuration for named parameters will be used.
  225. *
  226. * @param string $args A string with the passed & named params. eg. /1/page:2
  227. * @param string $context The current route context, which should contain controller/action keys.
  228. * @return array Array of ($pass, $named)
  229. */
  230. protected function _parseArgs($args, $context) {
  231. $pass = $named = array();
  232. $args = explode('/', $args);
  233. $namedConfig = Router::namedConfig();
  234. $greedy = $namedConfig['greedyNamed'];
  235. $rules = $namedConfig['rules'];
  236. if (!empty($this->options['named'])) {
  237. $greedy = isset($this->options['greedyNamed']) && $this->options['greedyNamed'] === true;
  238. foreach ((array)$this->options['named'] as $key => $val) {
  239. if (is_numeric($key)) {
  240. $rules[$val] = true;
  241. continue;
  242. }
  243. $rules[$key] = $val;
  244. }
  245. }
  246. foreach ($args as $param) {
  247. if (empty($param) && $param !== '0' && $param !== 0) {
  248. continue;
  249. }
  250. $separatorIsPresent = strpos($param, $namedConfig['separator']) !== false;
  251. if ((!isset($this->options['named']) || !empty($this->options['named'])) && $separatorIsPresent) {
  252. list($key, $val) = explode($namedConfig['separator'], $param, 2);
  253. $val = rawurldecode($val);
  254. $hasRule = isset($rules[$key]);
  255. $passIt = (!$hasRule && !$greedy) || ($hasRule && !$this->_matchNamed($val, $rules[$key], $context));
  256. if ($passIt) {
  257. $pass[] = rawurldecode($param);
  258. } else {
  259. if (preg_match_all('/\[([A-Za-z0-9_-]+)?\]/', $key, $matches, PREG_SET_ORDER)) {
  260. $matches = array_reverse($matches);
  261. $parts = explode('[', $key);
  262. $key = array_shift($parts);
  263. $arr = $val;
  264. foreach ($matches as $match) {
  265. if (empty($match[1])) {
  266. $arr = array($arr);
  267. } else {
  268. $arr = array(
  269. $match[1] => $arr
  270. );
  271. }
  272. }
  273. $val = $arr;
  274. }
  275. $named = array_merge_recursive($named, array($key => $val));
  276. }
  277. } else {
  278. $pass[] = rawurldecode($param);
  279. }
  280. }
  281. return array($pass, $named);
  282. }
  283. /**
  284. * Return true if a given named $param's $val matches a given $rule depending on $context. Currently implemented
  285. * rule types are controller, action and match that can be combined with each other.
  286. *
  287. * @param string $val The value of the named parameter
  288. * @param array $rule The rule(s) to apply, can also be a match string
  289. * @param string $context An array with additional context information (controller / action)
  290. * @return boolean
  291. */
  292. protected function _matchNamed($val, $rule, $context) {
  293. if ($rule === true || $rule === false) {
  294. return $rule;
  295. }
  296. if (is_string($rule)) {
  297. $rule = array('match' => $rule);
  298. }
  299. if (!is_array($rule)) {
  300. return false;
  301. }
  302. $controllerMatches = (
  303. !isset($rule['controller'], $context['controller']) ||
  304. in_array($context['controller'], (array)$rule['controller'])
  305. );
  306. if (!$controllerMatches) {
  307. return false;
  308. }
  309. $actionMatches = (
  310. !isset($rule['action'], $context['action']) ||
  311. in_array($context['action'], (array)$rule['action'])
  312. );
  313. if (!$actionMatches) {
  314. return false;
  315. }
  316. return (!isset($rule['match']) || preg_match('/' . $rule['match'] . '/', $val));
  317. }
  318. /**
  319. * Apply persistent parameters to a url array. Persistent parameters are a special
  320. * key used during route creation to force route parameters to persist when omitted from
  321. * a url array.
  322. *
  323. * @param array $url The array to apply persistent parameters to.
  324. * @param array $params An array of persistent values to replace persistent ones.
  325. * @return array An array with persistent parameters applied.
  326. */
  327. public function persistParams($url, $params) {
  328. foreach ($this->options['persist'] as $persistKey) {
  329. if (array_key_exists($persistKey, $params) && !isset($url[$persistKey])) {
  330. $url[$persistKey] = $params[$persistKey];
  331. }
  332. }
  333. return $url;
  334. }
  335. /**
  336. * Attempt to match a url array. If the url matches the route parameters and settings, then
  337. * return a generated string url. If the url doesn't match the route parameters, false will be returned.
  338. * This method handles the reverse routing or conversion of url arrays into string urls.
  339. *
  340. * @param array $url An array of parameters to check matching with.
  341. * @return mixed Either a string url for the parameters if they match or false.
  342. */
  343. public function match($url) {
  344. if (!$this->compiled()) {
  345. $this->compile();
  346. }
  347. $defaults = $this->defaults;
  348. if (isset($defaults['prefix'])) {
  349. $url['prefix'] = $defaults['prefix'];
  350. }
  351. //check that all the key names are in the url
  352. $keyNames = array_flip($this->keys);
  353. if (array_intersect_key($keyNames, $url) !== $keyNames) {
  354. return false;
  355. }
  356. // Missing defaults is a fail.
  357. if (array_diff_key($defaults, $url) !== array()) {
  358. return false;
  359. }
  360. $namedConfig = Router::namedConfig();
  361. $prefixes = Router::prefixes();
  362. $greedyNamed = $namedConfig['greedyNamed'];
  363. $allowedNamedParams = $namedConfig['rules'];
  364. $named = $pass = array();
  365. foreach ($url as $key => $value) {
  366. // keys that exist in the defaults and have different values is a match failure.
  367. $defaultExists = array_key_exists($key, $defaults);
  368. if ($defaultExists && $defaults[$key] != $value) {
  369. return false;
  370. } elseif ($defaultExists) {
  371. continue;
  372. }
  373. // If the key is a routed key, its not different yet.
  374. if (array_key_exists($key, $keyNames)) {
  375. continue;
  376. }
  377. // pull out passed args
  378. $numeric = is_numeric($key);
  379. if ($numeric && isset($defaults[$key]) && $defaults[$key] == $value) {
  380. continue;
  381. } elseif ($numeric) {
  382. $pass[] = $value;
  383. unset($url[$key]);
  384. continue;
  385. }
  386. // pull out named params if named params are greedy or a rule exists.
  387. if (
  388. ($greedyNamed || isset($allowedNamedParams[$key])) &&
  389. ($value !== false && $value !== null) &&
  390. (!in_array($key, $prefixes))
  391. ) {
  392. $named[$key] = $value;
  393. continue;
  394. }
  395. // keys that don't exist are different.
  396. if (!$defaultExists && !empty($value)) {
  397. return false;
  398. }
  399. }
  400. //if a not a greedy route, no extra params are allowed.
  401. if (!$this->_greedy && (!empty($pass) || !empty($named))) {
  402. return false;
  403. }
  404. //check patterns for routed params
  405. if (!empty($this->options)) {
  406. foreach ($this->options as $key => $pattern) {
  407. if (array_key_exists($key, $url) && !preg_match('#^' . $pattern . '$#', $url[$key])) {
  408. return false;
  409. }
  410. }
  411. }
  412. return $this->_writeUrl(array_merge($url, compact('pass', 'named')));
  413. }
  414. /**
  415. * Converts a matching route array into a url string. Composes the string url using the template
  416. * used to create the route.
  417. *
  418. * @param array $params The params to convert to a string url.
  419. * @return string Composed route string.
  420. */
  421. protected function _writeUrl($params) {
  422. if (isset($params['prefix'], $params['action'])) {
  423. $params['action'] = str_replace($params['prefix'] . '_', '', $params['action']);
  424. unset($params['prefix']);
  425. }
  426. if (is_array($params['pass'])) {
  427. $params['pass'] = implode('/', array_map('rawurlencode', $params['pass']));
  428. }
  429. $namedConfig = Router::namedConfig();
  430. $separator = $namedConfig['separator'];
  431. if (!empty($params['named']) && is_array($params['named'])) {
  432. $named = array();
  433. foreach ($params['named'] as $key => $value) {
  434. if (is_array($value)) {
  435. $flat = Set::flatten($value, '][');
  436. foreach ($flat as $namedKey => $namedValue) {
  437. $named[] = $key . "[$namedKey]" . $separator . rawurlencode($namedValue);
  438. }
  439. } else {
  440. $named[] = $key . $separator . rawurlencode($value);
  441. }
  442. }
  443. $params['pass'] = $params['pass'] . '/' . implode('/', $named);
  444. }
  445. $out = $this->template;
  446. $search = $replace = array();
  447. foreach ($this->keys as $key) {
  448. $string = null;
  449. if (isset($params[$key])) {
  450. $string = $params[$key];
  451. } elseif (strpos($out, $key) != strlen($out) - strlen($key)) {
  452. $key .= '/';
  453. }
  454. $search[] = ':' . $key;
  455. $replace[] = $string;
  456. }
  457. $out = str_replace($search, $replace, $out);
  458. if (strpos($this->template, '*')) {
  459. $out = str_replace('*', $params['pass'], $out);
  460. }
  461. $out = str_replace('//', '/', $out);
  462. return $out;
  463. }
  464. }