PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

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

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