PageRenderTime 59ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/cake/libs/router.php

https://github.com/hardsshah/bookmarks
PHP | 1368 lines | 841 code | 86 blank | 441 comment | 257 complexity | cec8df13c9044040e65391a8ff9b3eea MD5 | raw file
  1. <?php
  2. /* SVN FILE: $Id$ */
  3. /**
  4. * Parses the request URL into controller, action, and parameters.
  5. *
  6. * Long description for file
  7. *
  8. * PHP versions 4 and 5
  9. *
  10. * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org)
  11. * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
  12. *
  13. * Licensed under The MIT License
  14. * Redistributions of files must retain the above copyright notice.
  15. *
  16. * @filesource
  17. * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
  18. * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
  19. * @package cake
  20. * @subpackage cake.cake.libs
  21. * @since CakePHP(tm) v 0.2.9
  22. * @version $Revision$
  23. * @modifiedby $LastChangedBy$
  24. * @lastmodified $Date$
  25. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  26. */
  27. /**
  28. * Included libraries.
  29. *
  30. */
  31. if (!class_exists('Object')) {
  32. App::import('Core', 'Object');
  33. }
  34. /**
  35. * Parses the request URL into controller, action, and parameters.
  36. *
  37. * @package cake
  38. * @subpackage cake.cake.libs
  39. */
  40. class Router extends Object {
  41. /**
  42. * Array of routes
  43. *
  44. * @var array
  45. * @access public
  46. */
  47. var $routes = array();
  48. /**
  49. * Caches admin setting from Configure class
  50. *
  51. * @var array
  52. * @access private
  53. */
  54. var $__admin = null;
  55. /**
  56. * List of action prefixes used in connected routes
  57. *
  58. * @var array
  59. * @access private
  60. */
  61. var $__prefixes = array();
  62. /**
  63. * Directive for Router to parse out file extensions for mapping to Content-types.
  64. *
  65. * @var boolean
  66. * @access private
  67. */
  68. var $__parseExtensions = false;
  69. /**
  70. * List of valid extensions to parse from a URL. If null, any extension is allowed.
  71. *
  72. * @var array
  73. * @access private
  74. */
  75. var $__validExtensions = null;
  76. /**
  77. * 'Constant' regular expression definitions for named route elements
  78. *
  79. * @var array
  80. * @access private
  81. */
  82. var $__named = array(
  83. 'Action' => 'index|show|add|create|edit|update|remove|del|delete|view|item',
  84. 'Year' => '[12][0-9]{3}',
  85. 'Month' => '0[1-9]|1[012]',
  86. 'Day' => '0[1-9]|[12][0-9]|3[01]',
  87. 'ID' => '[0-9]+',
  88. '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}'
  89. );
  90. /**
  91. * Stores all information necessary to decide what named arguments are parsed under what conditions.
  92. *
  93. * @var string
  94. * @access public
  95. */
  96. var $named = array(
  97. 'default' => array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step'),
  98. 'greedy' => true,
  99. 'separator' => ':',
  100. 'rules' => false,
  101. );
  102. /**
  103. * The route matching the URL of the current request
  104. *
  105. * @var array
  106. * @access private
  107. */
  108. var $__currentRoute = array();
  109. /**
  110. * HTTP header shortcut map. Used for evaluating header-based route expressions.
  111. *
  112. * @var array
  113. * @access private
  114. */
  115. var $__headerMap = array(
  116. 'type' => 'content_type',
  117. 'method' => 'request_method',
  118. 'server' => 'server_name'
  119. );
  120. /**
  121. * Default HTTP request method => controller action map.
  122. *
  123. * @var array
  124. * @access private
  125. */
  126. var $__resourceMap = array(
  127. array('action' => 'index', 'method' => 'GET', 'id' => false),
  128. array('action' => 'view', 'method' => 'GET', 'id' => true),
  129. array('action' => 'add', 'method' => 'POST', 'id' => false),
  130. array('action' => 'edit', 'method' => 'PUT', 'id' => true),
  131. array('action' => 'delete', 'method' => 'DELETE', 'id' => true),
  132. array('action' => 'edit', 'method' => 'POST', 'id' => true)
  133. );
  134. /**
  135. * List of resource-mapped controllers
  136. *
  137. * @var array
  138. * @access private
  139. */
  140. var $__resourceMapped = array();
  141. /**
  142. * Maintains the parameter stack for the current request
  143. *
  144. * @var array
  145. * @access private
  146. */
  147. var $__params = array();
  148. /**
  149. * Maintains the path stack for the current request
  150. *
  151. * @var array
  152. * @access private
  153. */
  154. var $__paths = array();
  155. /**
  156. * Keeps Router state to determine if default routes have already been connected
  157. *
  158. * @var boolean
  159. * @access private
  160. */
  161. var $__defaultsMapped = false;
  162. /**
  163. * Gets a reference to the Router object instance
  164. *
  165. * @return object Object instance
  166. * @access public
  167. * @static
  168. */
  169. function &getInstance() {
  170. static $instance = array();
  171. if (!$instance) {
  172. $instance[0] =& new Router();
  173. $instance[0]->__admin = Configure::read('Routing.admin');
  174. }
  175. return $instance[0];
  176. }
  177. /**
  178. * Gets the named route elements for use in app/config/routes.php
  179. *
  180. * @return array Named route elements
  181. * @access public
  182. * @see Router::$__named
  183. * @static
  184. */
  185. function getNamedExpressions() {
  186. $_this =& Router::getInstance();
  187. return $_this->__named;
  188. }
  189. /**
  190. * Returns this object's routes array. Returns false if there are no routes available.
  191. *
  192. * @param string $route An empty string, or a route string "/"
  193. * @param array $default NULL or an array describing the default route
  194. * @param array $params An array matching the named elements in the route to regular expressions which that element should match.
  195. * @see routes
  196. * @return array Array of routes
  197. * @access public
  198. * @static
  199. */
  200. function connect($route, $default = array(), $params = array()) {
  201. $_this =& Router::getInstance();
  202. if (!isset($default['action'])) {
  203. $default['action'] = 'index';
  204. }
  205. if (isset($default[$_this->__admin])) {
  206. $default['prefix'] = $_this->__admin;
  207. }
  208. if (isset($default['prefix'])) {
  209. $_this->__prefixes[] = $default['prefix'];
  210. $_this->__prefixes = array_keys(array_flip($_this->__prefixes));
  211. }
  212. $_this->routes[] = array($route, $default, $params);
  213. return $_this->routes;
  214. }
  215. /**
  216. * Specifies what named parameters CakePHP should be parsing. The most common setups are:
  217. *
  218. * Do not parse any named parameters:
  219. * {{{ Router::connectNamed(false); }}}
  220. *
  221. * Parse only default parameters used for CakePHP's pagination:
  222. * {{{ Router::connectNamed(false, array('default' => true)); }}}
  223. *
  224. * Parse only the page parameter if its value is a number:
  225. * {{{ Router::connectNamed(array('page' => '[\d]+'), array('default' => false, 'greedy' => false)); }}}
  226. *
  227. * Parse only the page parameter no mater what.
  228. * {{{ Router::connectNamed(array('page'), array('default' => false, 'greedy' => false)); }}}
  229. *
  230. * Parse only the page parameter if the current action is 'index'.
  231. * {{{ Router::connectNamed(array('page' => array('action' => 'index')), array('default' => false, 'greedy' => false)); }}}
  232. *
  233. * Parse only the page parameter if the current action is 'index' and the controller is 'pages'.
  234. * {{{ Router::connectNamed(array('page' => array('action' => 'index', 'controller' => 'pages')), array('default' => false, 'greedy' => false)); }}}
  235. *
  236. * @param array $named A list of named parameters. Key value pairs are accepted where values are either regex strings to match, or arrays as seen above.
  237. * @param array $options Allows to control all settings: separator, greedy, reset, default
  238. * @return array
  239. * @access public
  240. * @static
  241. */
  242. function connectNamed($named, $options = array()) {
  243. $_this =& Router::getInstance();
  244. if (isset($options['argSeparator'])) {
  245. $_this->named['separator'] = $options['argSeparator'];
  246. unset($options['argSeparator']);
  247. }
  248. if ($named === true || $named === false) {
  249. $options = array_merge(array('default' => $named, 'reset' => true, 'greedy' => $named), $options);
  250. $named = array();
  251. }
  252. $options = array_merge(array('default' => false, 'reset' => false, 'greedy' => true), $options);
  253. if ($options['reset'] == true || $_this->named['rules'] === false) {
  254. $_this->named['rules'] = array();
  255. }
  256. if ($options['default']) {
  257. $named = array_merge($named, $_this->named['default']);
  258. }
  259. foreach ($named as $key => $val) {
  260. if (is_numeric($key)) {
  261. $_this->named['rules'][$val] = true;
  262. } else {
  263. $_this->named['rules'][$key] = $val;
  264. }
  265. }
  266. $_this->named['greedy'] = $options['greedy'];
  267. return $_this->named;
  268. }
  269. /**
  270. * Creates REST resource routes for the given controller(s)
  271. *
  272. * Options:
  273. *
  274. * - 'id' - The regular expression fragment to use when matching IDs. By default, matches
  275. * integer values and UUIDs.
  276. * - 'prefix' - URL prefix to use for the generated routes. Defaults to '/'.
  277. *
  278. * @param mixed $controller A controller name or array of controller names (i.e. "Posts" or "ListItems")
  279. * @param array $options Options to use when generating REST routes
  280. * @return void
  281. * @access public
  282. * @static
  283. */
  284. function mapResources($controller, $options = array()) {
  285. $_this =& Router::getInstance();
  286. $options = array_merge(array('prefix' => '/', 'id' => $_this->__named['ID'] . '|' . $_this->__named['UUID']), $options);
  287. $prefix = $options['prefix'];
  288. foreach ((array)$controller as $ctlName) {
  289. $urlName = Inflector::underscore($ctlName);
  290. foreach ($_this->__resourceMap as $params) {
  291. extract($params);
  292. $url = $prefix . $urlName . (($id) ? '/:id' : '');
  293. Router::connect($url,
  294. array('controller' => $urlName, 'action' => $action, '[method]' => $params['method']),
  295. array('id' => $options['id'], 'pass' => array('id'))
  296. );
  297. }
  298. $_this->__resourceMapped[] = $urlName;
  299. }
  300. }
  301. /**
  302. * Builds a route regular expression
  303. *
  304. * @param string $route An empty string, or a route string "/"
  305. * @param array $default NULL or an array describing the default route
  306. * @param array $params An array matching the named elements in the route to regular expressions which that element should match.
  307. * @return array
  308. * @see routes
  309. * @access public
  310. * @static
  311. */
  312. function writeRoute($route, $default, $params) {
  313. if (empty($route) || ($route === '/')) {
  314. return array('/^[\/]*$/', array());
  315. }
  316. $names = array();
  317. $elements = explode('/', $route);
  318. foreach ($elements as $element) {
  319. if (empty($element)) {
  320. continue;
  321. }
  322. $q = null;
  323. $element = trim($element);
  324. $namedParam = strpos($element, ':') !== false;
  325. if ($namedParam && preg_match('/^:([^:]+)$/', $element, $r)) {
  326. if (isset($params[$r[1]])) {
  327. if ($r[1] != 'plugin' && array_key_exists($r[1], $default)) {
  328. $q = '?';
  329. }
  330. $parsed[] = '(?:/(' . $params[$r[1]] . ')' . $q . ')' . $q;
  331. } else {
  332. $parsed[] = '(?:/([^\/]+))?';
  333. }
  334. $names[] = $r[1];
  335. } elseif ($element === '*') {
  336. $parsed[] = '(?:/(.*))?';
  337. } else if ($namedParam && preg_match_all('/(?!\\\\):([a-z_0-9]+)/i', $element, $matches)) {
  338. $matchCount = count($matches[1]);
  339. foreach ($matches[1] as $i => $name) {
  340. $pos = strpos($element, ':' . $name);
  341. $before = substr($element, 0, $pos);
  342. $element = substr($element, $pos + strlen($name) + 1);
  343. $after = null;
  344. if ($i + 1 === $matchCount && $element) {
  345. $after = preg_quote($element);
  346. }
  347. if ($i === 0) {
  348. $before = '/' . $before;
  349. }
  350. $before = preg_quote($before, '#');
  351. if (isset($params[$name])) {
  352. if (isset($default[$name]) && $name != 'plugin') {
  353. $q = '?';
  354. }
  355. $parsed[] = '(?:' . $before . '(' . $params[$name] . ')' . $q . $after . ')' . $q;
  356. } else {
  357. $parsed[] = '(?:' . $before . '([^\/]+)' . $after . ')?';
  358. }
  359. $names[] = $name;
  360. }
  361. } else {
  362. $parsed[] = '/' . $element;
  363. }
  364. }
  365. return array('#^' . join('', $parsed) . '[\/]*$#', $names);
  366. }
  367. /**
  368. * Returns the list of prefixes used in connected routes
  369. *
  370. * @return array A list of prefixes used in connected routes
  371. * @access public
  372. * @static
  373. */
  374. function prefixes() {
  375. $_this =& Router::getInstance();
  376. return $_this->__prefixes;
  377. }
  378. /**
  379. * Parses given URL and returns an array of controllers, action and parameters
  380. * taken from that URL.
  381. *
  382. * @param string $url URL to be parsed
  383. * @return array Parsed elements from URL
  384. * @access public
  385. * @static
  386. */
  387. function parse($url) {
  388. $_this =& Router::getInstance();
  389. if (!$_this->__defaultsMapped) {
  390. $_this->__connectDefaultRoutes();
  391. }
  392. $out = array('pass' => array(), 'named' => array());
  393. $r = $ext = null;
  394. if (ini_get('magic_quotes_gpc') === '1') {
  395. $url = stripslashes_deep($url);
  396. }
  397. if ($url && strpos($url, '/') !== 0) {
  398. $url = '/' . $url;
  399. }
  400. if (strpos($url, '?') !== false) {
  401. $url = substr($url, 0, strpos($url, '?'));
  402. }
  403. extract($_this->__parseExtension($url));
  404. foreach ($_this->routes as $i => $route) {
  405. if (count($route) === 3) {
  406. $route = $_this->compile($i);
  407. }
  408. if (($r = $_this->__matchRoute($route, $url)) !== false) {
  409. $_this->__currentRoute[] = $route;
  410. list($route, $regexp, $names, $defaults, $params) = $route;
  411. $argOptions = array();
  412. if (array_key_exists('named', $params)) {
  413. $argOptions['named'] = $params['named'];
  414. unset($params['named']);
  415. }
  416. if (array_key_exists('greedy', $params)) {
  417. $argOptions['greedy'] = $params['greedy'];
  418. unset($params['greedy']);
  419. }
  420. array_shift($r);
  421. foreach ($names as $name) {
  422. $out[$name] = null;
  423. }
  424. if (is_array($defaults)) {
  425. foreach ($defaults as $name => $value) {
  426. if (preg_match('#[a-zA-Z_\-]#i', $name)) {
  427. $out[$name] = $value;
  428. } else {
  429. $out['pass'][] = $value;
  430. }
  431. }
  432. }
  433. foreach ($r as $key => $found) {
  434. if (empty($found) && $found != 0) {
  435. continue;
  436. }
  437. if (isset($names[$key])) {
  438. $out[$names[$key]] = $_this->stripEscape($found);
  439. } elseif (isset($names[$key]) && empty($names[$key]) && empty($out[$names[$key]])) {
  440. break;
  441. } else {
  442. $argOptions['context'] = array('action' => $out['action'], 'controller' => $out['controller']);
  443. extract($_this->getArgs($found, $argOptions));
  444. $out['pass'] = array_merge($out['pass'], $pass);
  445. $out['named'] = $named;
  446. }
  447. }
  448. if (isset($params['pass'])) {
  449. for ($j = count($params['pass']) - 1; $j > -1; $j--) {
  450. if (isset($out[$params['pass'][$j]])) {
  451. array_unshift($out['pass'], $out[$params['pass'][$j]]);
  452. }
  453. }
  454. }
  455. break;
  456. }
  457. }
  458. if (!empty($ext)) {
  459. $out['url']['ext'] = $ext;
  460. }
  461. return $out;
  462. }
  463. /**
  464. * Checks to see if the given URL matches the given route
  465. *
  466. * @param array $route
  467. * @param string $url
  468. * @return mixed Boolean false on failure, otherwise array
  469. * @access private
  470. */
  471. function __matchRoute($route, $url) {
  472. list($route, $regexp, $names, $defaults) = $route;
  473. if (!preg_match($regexp, $url, $r)) {
  474. return false;
  475. } else {
  476. foreach ($defaults as $key => $val) {
  477. if ($key{0} === '[' && preg_match('/^\[(\w+)\]$/', $key, $header)) {
  478. if (isset($this->__headerMap[$header[1]])) {
  479. $header = $this->__headerMap[$header[1]];
  480. } else {
  481. $header = 'http_' . $header[1];
  482. }
  483. $val = (array)$val;
  484. $h = false;
  485. foreach ($val as $v) {
  486. if (env(strtoupper($header)) === $v) {
  487. $h = true;
  488. }
  489. }
  490. if (!$h) {
  491. return false;
  492. }
  493. }
  494. }
  495. }
  496. return $r;
  497. }
  498. /**
  499. * Compiles a route by numeric key and returns the compiled expression, replacing
  500. * the existing uncompiled route. Do not call statically.
  501. *
  502. * @param integer $i
  503. * @return array Returns an array containing the compiled route
  504. * @access public
  505. */
  506. function compile($i) {
  507. $route = $this->routes[$i];
  508. if (!list($pattern, $names) = $this->writeRoute($route[0], $route[1], $route[2])) {
  509. unset($this->routes[$i]);
  510. return array();
  511. }
  512. $this->routes[$i] = array(
  513. $route[0], $pattern, $names,
  514. array_merge(array('plugin' => null, 'controller' => null), (array)$route[1]),
  515. $route[2]
  516. );
  517. return $this->routes[$i];
  518. }
  519. /**
  520. * Parses a file extension out of a URL, if Router::parseExtensions() is enabled.
  521. *
  522. * @param string $url
  523. * @return array Returns an array containing the altered URL and the parsed extension.
  524. * @access private
  525. */
  526. function __parseExtension($url) {
  527. $ext = null;
  528. if ($this->__parseExtensions) {
  529. if (preg_match('/\.[0-9a-zA-Z]*$/', $url, $match) === 1) {
  530. $match = substr($match[0], 1);
  531. if (empty($this->__validExtensions)) {
  532. $url = substr($url, 0, strpos($url, '.' . $match));
  533. $ext = $match;
  534. } else {
  535. foreach ($this->__validExtensions as $name) {
  536. if (strcasecmp($name, $match) === 0) {
  537. $url = substr($url, 0, strpos($url, '.' . $name));
  538. $ext = $match;
  539. }
  540. }
  541. }
  542. }
  543. if (empty($ext)) {
  544. $ext = 'html';
  545. }
  546. }
  547. return compact('ext', 'url');
  548. }
  549. /**
  550. * Connects the default, built-in routes, including admin routes, and (deprecated) web services
  551. * routes.
  552. *
  553. * @return void
  554. * @access private
  555. */
  556. function __connectDefaultRoutes() {
  557. if ($this->__defaultsMapped) {
  558. return;
  559. }
  560. if ($this->__admin) {
  561. $params = array('prefix' => $this->__admin, $this->__admin => true);
  562. }
  563. if ($plugins = Configure::listObjects('plugin')) {
  564. foreach ($plugins as $key => $value) {
  565. $plugins[$key] = Inflector::underscore($value);
  566. }
  567. $match = array('plugin' => implode('|', $plugins));
  568. $this->connect('/:plugin/:controller/:action/*', array(), $match);
  569. if ($this->__admin) {
  570. $this->connect("/{$this->__admin}/:plugin/:controller", $params, $match);
  571. $this->connect("/{$this->__admin}/:plugin/:controller/:action/*", $params, $match);
  572. }
  573. }
  574. if ($this->__admin) {
  575. $this->connect("/{$this->__admin}/:controller", $params);
  576. $this->connect("/{$this->__admin}/:controller/:action/*", $params);
  577. }
  578. $this->connect('/:controller', array('action' => 'index'));
  579. $this->connect('/:controller/:action/*');
  580. if ($this->named['rules'] === false) {
  581. $this->connectNamed(true);
  582. }
  583. $this->__defaultsMapped = true;
  584. }
  585. /**
  586. * Takes parameter and path information back from the Dispatcher
  587. *
  588. * @param array $params Parameters and path information
  589. * @return void
  590. * @access public
  591. * @static
  592. */
  593. function setRequestInfo($params) {
  594. $_this =& Router::getInstance();
  595. $defaults = array('plugin' => null, 'controller' => null, 'action' => null);
  596. $params[0] = array_merge($defaults, (array)$params[0]);
  597. $params[1] = array_merge($defaults, (array)$params[1]);
  598. list($_this->__params[], $_this->__paths[]) = $params;
  599. if (count($_this->__paths)) {
  600. if (isset($_this->__paths[0]['namedArgs'])) {
  601. foreach ($_this->__paths[0]['namedArgs'] as $arg => $value) {
  602. $_this->named['rules'][$arg] = true;
  603. }
  604. }
  605. }
  606. }
  607. /**
  608. * Gets parameter information
  609. *
  610. * @param boolean $current Get current parameter (true)
  611. * @return array Parameter information
  612. * @access public
  613. * @static
  614. */
  615. function getParams($current = false) {
  616. $_this =& Router::getInstance();
  617. if ($current) {
  618. return $_this->__params[count($_this->__params) - 1];
  619. }
  620. if (isset($_this->__params[0])) {
  621. return $_this->__params[0];
  622. }
  623. return array();
  624. }
  625. /**
  626. * Gets URL parameter by name
  627. *
  628. * @param string $name Parameter name
  629. * @param boolean $current Current parameter
  630. * @return string Parameter value
  631. * @access public
  632. * @static
  633. */
  634. function getParam($name = 'controller', $current = false) {
  635. $params = Router::getParams($current);
  636. if (isset($params[$name])) {
  637. return $params[$name];
  638. }
  639. return null;
  640. }
  641. /**
  642. * Gets path information
  643. *
  644. * @param boolean $current Current parameter
  645. * @return array
  646. * @access public
  647. * @static
  648. */
  649. function getPaths($current = false) {
  650. $_this =& Router::getInstance();
  651. if ($current) {
  652. return $_this->__paths[count($_this->__paths) - 1];
  653. }
  654. if (!isset($_this->__paths[0])) {
  655. return array('base' => null);
  656. }
  657. return $_this->__paths[0];
  658. }
  659. /**
  660. * Reloads default Router settings
  661. *
  662. * @access public
  663. * @return void
  664. * @static
  665. */
  666. function reload() {
  667. $_this =& Router::getInstance();
  668. foreach (get_class_vars('Router') as $key => $val) {
  669. $_this->{$key} = $val;
  670. }
  671. $_this->__admin = Configure::read('Routing.admin');
  672. }
  673. /**
  674. * Promote a route (by default, the last one added) to the beginning of the list
  675. *
  676. * @param $which A zero-based array index representing the route to move. For example,
  677. * if 3 routes have been added, the last route would be 2.
  678. * @return boolean Retuns false if no route exists at the position specified by $which.
  679. * @access public
  680. * @static
  681. */
  682. function promote($which = null) {
  683. $_this =& Router::getInstance();
  684. if ($which === null) {
  685. $which = count($_this->routes) - 1;
  686. }
  687. if (!isset($_this->routes[$which])) {
  688. return false;
  689. }
  690. $route = $_this->routes[$which];
  691. unset($_this->routes[$which]);
  692. array_unshift($_this->routes, $route);
  693. return true;
  694. }
  695. /**
  696. * Finds URL for specified action.
  697. *
  698. * Returns an URL pointing to a combination of controller and action. Param
  699. * $url can be:
  700. *
  701. * - Empty - the method will find adress to actuall controller/action.
  702. * - '/' - the method will find base URL of application.
  703. * - A combination of controller/action - the method will find url for it.
  704. *
  705. * @param mixed $url Cake-relative URL, like "/products/edit/92" or "/presidents/elect/4"
  706. * or an array specifying any of the following: 'controller', 'action',
  707. * and/or 'plugin', in addition to named arguments (keyed array elements),
  708. * and standard URL arguments (indexed array elements)
  709. * @param mixed $full If (bool) true, the full base URL will be prepended to the result.
  710. * If an array accepts the following keys
  711. * - escape - used when making urls embedded in html escapes query string '&'
  712. * - full - if true the full base URL will be prepended.
  713. * @return string Full translated URL with base path.
  714. * @access public
  715. * @static
  716. */
  717. function url($url = null, $full = false) {
  718. $_this =& Router::getInstance();
  719. $defaults = $params = array('plugin' => null, 'controller' => null, 'action' => 'index');
  720. if (is_bool($full)) {
  721. $escape = false;
  722. } else {
  723. extract(array_merge(array('escape' => false, 'full' => false), $full));
  724. }
  725. if (!empty($_this->__params)) {
  726. if (isset($this) && !isset($this->params['requested'])) {
  727. $params = $_this->__params[0];
  728. } else {
  729. $params = end($_this->__params);
  730. }
  731. }
  732. $path = array('base' => null);
  733. if (!empty($_this->__paths)) {
  734. if (isset($this) && !isset($this->params['requested'])) {
  735. $path = $_this->__paths[0];
  736. } else {
  737. $path = end($_this->__paths);
  738. }
  739. }
  740. $base = $path['base'];
  741. $extension = $output = $mapped = $q = $frag = null;
  742. if (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 = '#' . urlencode($url['#']);
  757. unset($url['#']);
  758. }
  759. if (empty($url['action'])) {
  760. if (empty($url['controller']) || $params['controller'] === $url['controller']) {
  761. $url['action'] = $params['action'];
  762. } else {
  763. $url['action'] = 'index';
  764. }
  765. }
  766. if ($_this->__admin) {
  767. if (!isset($url[$_this->__admin]) && !empty($params[$_this->__admin])) {
  768. $url[$_this->__admin] = true;
  769. } elseif ($_this->__admin && isset($url[$_this->__admin]) && !$url[$_this->__admin]) {
  770. unset($url[$_this->__admin]);
  771. }
  772. }
  773. $plugin = false;
  774. if (array_key_exists('plugin', $url)) {
  775. $plugin = $url['plugin'];
  776. }
  777. $url = array_merge(array('controller' => $params['controller'], 'plugin' => $params['plugin']), Set::filter($url, true));
  778. if ($plugin !== false) {
  779. $url['plugin'] = $plugin;
  780. }
  781. if (isset($url['ext'])) {
  782. $extension = '.' . $url['ext'];
  783. unset($url['ext']);
  784. }
  785. $match = false;
  786. foreach ($_this->routes as $i => $route) {
  787. if (count($route) === 3) {
  788. $route = $_this->compile($i);
  789. }
  790. $originalUrl = $url;
  791. if (isset($route[4]['persist'], $_this->__params[0])) {
  792. $url = array_merge(array_intersect_key($params, Set::combine($route[4]['persist'], '/')), $url);
  793. }
  794. if ($match = $_this->mapRouteElements($route, $url)) {
  795. $output = trim($match, '/');
  796. $url = array();
  797. break;
  798. }
  799. $url = $originalUrl;
  800. }
  801. $named = $args = array();
  802. $skip = array(
  803. 'bare', 'action', 'controller', 'plugin', 'ext', '?', '#', 'prefix', $_this->__admin
  804. );
  805. $keys = array_values(array_diff(array_keys($url), $skip));
  806. $count = count($keys);
  807. // Remove this once parsed URL parameters can be inserted into 'pass'
  808. for ($i = 0; $i < $count; $i++) {
  809. if ($i === 0 && is_numeric($keys[$i]) && in_array('id', $keys)) {
  810. $args[0] = $url[$keys[$i]];
  811. } elseif (is_numeric($keys[$i]) || $keys[$i] === 'id') {
  812. $args[] = $url[$keys[$i]];
  813. } else {
  814. $named[$keys[$i]] = $url[$keys[$i]];
  815. }
  816. }
  817. if ($match === false) {
  818. list($args, $named) = array(Set::filter($args, true), Set::filter($named));
  819. if (!empty($url[$_this->__admin])) {
  820. $url['action'] = str_replace($_this->__admin . '_', '', $url['action']);
  821. }
  822. if (empty($named) && empty($args) && (!isset($url['action']) || $url['action'] === 'index')) {
  823. $url['action'] = null;
  824. }
  825. $urlOut = Set::filter(array($url['controller'], $url['action']));
  826. if (isset($url['plugin']) && $url['plugin'] != $url['controller']) {
  827. array_unshift($urlOut, $url['plugin']);
  828. }
  829. if ($_this->__admin && isset($url[$_this->__admin])) {
  830. array_unshift($urlOut, $_this->__admin);
  831. }
  832. $output = join('/', $urlOut) . '/';
  833. }
  834. if (!empty($args)) {
  835. $args = join('/', $args);
  836. if ($output{strlen($output) - 1} != '/') {
  837. $args = '/'. $args;
  838. }
  839. $output .= $args;
  840. }
  841. if (!empty($named)) {
  842. foreach ($named as $name => $value) {
  843. $output .= '/' . $name . $_this->named['separator'] . $value;
  844. }
  845. }
  846. $output = str_replace('//', '/', $base . '/' . $output);
  847. } else {
  848. if (((strpos($url, '://')) || (strpos($url, 'javascript:') === 0) || (strpos($url, 'mailto:') === 0)) || (!strncmp($url, '#', 1))) {
  849. return $url;
  850. }
  851. if (empty($url)) {
  852. if (!isset($path['here'])) {
  853. $path['here'] = '/';
  854. }
  855. $output = $path['here'];
  856. } elseif (substr($url, 0, 1) === '/') {
  857. $output = $base . $url;
  858. } else {
  859. $output = $base . '/';
  860. if ($_this->__admin && isset($params[$_this->__admin])) {
  861. $output .= $_this->__admin . '/';
  862. }
  863. if (!empty($params['plugin']) && $params['plugin'] !== $params['controller']) {
  864. $output .= Inflector::underscore($params['plugin']) . '/';
  865. }
  866. $output .= Inflector::underscore($params['controller']) . '/' . $url;
  867. }
  868. $output = str_replace('//', '/', $output);
  869. }
  870. if ($full && defined('FULL_BASE_URL')) {
  871. $output = FULL_BASE_URL . $output;
  872. }
  873. if (!empty($extension) && substr($output, -1) === '/') {
  874. $output = substr($output, 0, -1);
  875. }
  876. return $output . $extension . $_this->queryString($q, array(), $escape) . $frag;
  877. }
  878. /**
  879. * Maps a URL array onto a route and returns the string result, or false if no match
  880. *
  881. * @param array $route Route Route
  882. * @param array $url URL URL to map
  883. * @return mixed Result (as string) or false if no match
  884. * @access public
  885. * @static
  886. */
  887. function mapRouteElements($route, $url) {
  888. if (isset($route[3]['prefix'])) {
  889. $prefix = $route[3]['prefix'];
  890. unset($route[3]['prefix']);
  891. }
  892. $pass = array();
  893. $defaults = $route[3];
  894. $routeParams = $route[2];
  895. $params = Set::diff($url, $defaults);
  896. $urlInv = array_combine(array_values($url), array_keys($url));
  897. $i = 0;
  898. while (isset($defaults[$i])) {
  899. if (isset($urlInv[$defaults[$i]])) {
  900. if (!in_array($defaults[$i], $url) && is_int($urlInv[$defaults[$i]])) {
  901. return false;
  902. }
  903. unset($urlInv[$defaults[$i]], $defaults[$i]);
  904. } else {
  905. return false;
  906. }
  907. $i++;
  908. }
  909. foreach ($params as $key => $value) {
  910. if (is_int($key)) {
  911. $pass[] = $value;
  912. unset($params[$key]);
  913. }
  914. }
  915. list($named, $params) = Router::getNamedElements($params);
  916. if (!strpos($route[0], '*') && (!empty($pass) || !empty($named))) {
  917. return false;
  918. }
  919. $urlKeys = array_keys($url);
  920. $paramsKeys = array_keys($params);
  921. $defaultsKeys = array_keys($defaults);
  922. if (!empty($params)) {
  923. if (array_diff($paramsKeys, $routeParams) != array()) {
  924. return false;
  925. }
  926. $required = array_values(array_diff($routeParams, $urlKeys));
  927. $reqCount = count($required);
  928. for ($i = 0; $i < $reqCount; $i++) {
  929. if (array_key_exists($required[$i], $defaults) && $defaults[$required[$i]] === null) {
  930. unset($required[$i]);
  931. }
  932. }
  933. }
  934. $isFilled = true;
  935. if (!empty($routeParams)) {
  936. $filled = array_intersect_key($url, array_combine($routeParams, array_keys($routeParams)));
  937. $isFilled = (array_diff($routeParams, array_keys($filled)) === array());
  938. if (!$isFilled && empty($params)) {
  939. return false;
  940. }
  941. }
  942. if (empty($params)) {
  943. return Router::__mapRoute($route, array_merge($url, compact('pass', 'named', 'prefix')));
  944. } elseif (!empty($routeParams) && !empty($route[3])) {
  945. if (!empty($required)) {
  946. return false;
  947. }
  948. foreach ($params as $key => $val) {
  949. if ((!isset($url[$key]) || $url[$key] != $val) || (!isset($defaults[$key]) || $defaults[$key] != $val) && !in_array($key, $routeParams)) {
  950. if (!isset($defaults[$key])) {
  951. continue;
  952. }
  953. return false;
  954. }
  955. }
  956. } else {
  957. if (empty($required) && $defaults['plugin'] === $url['plugin'] && $defaults['controller'] === $url['controller'] && $defaults['action'] === $url['action']) {
  958. return Router::__mapRoute($route, array_merge($url, compact('pass', 'named', 'prefix')));
  959. }
  960. return false;
  961. }
  962. if (!empty($route[4])) {
  963. foreach ($route[4] as $key => $reg) {
  964. if (array_key_exists($key, $url) && !preg_match('#' . $reg . '#', $url[$key])) {
  965. return false;
  966. }
  967. }
  968. }
  969. return Router::__mapRoute($route, array_merge($filled, compact('pass', 'named', 'prefix')));
  970. }
  971. /**
  972. * Merges URL parameters into a route string
  973. *
  974. * @param array $route Route
  975. * @param array $params Parameters
  976. * @return string Merged URL with parameters
  977. * @access private
  978. */
  979. function __mapRoute($route, $params = array()) {
  980. if (isset($params['plugin']) && isset($params['controller']) && $params['plugin'] === $params['controller']) {
  981. unset($params['controller']);
  982. }
  983. if (isset($params['prefix']) && isset($params['action'])) {
  984. $params['action'] = str_replace($params['prefix'] . '_', '', $params['action']);
  985. unset($params['prefix']);
  986. }
  987. if (isset($params['pass']) && is_array($params['pass'])) {
  988. $params['pass'] = implode('/', Set::filter($params['pass'], true));
  989. } elseif (!isset($params['pass'])) {
  990. $params['pass'] = '';
  991. }
  992. if (isset($params['named'])) {
  993. if (is_array($params['named'])) {
  994. $count = count($params['named']);
  995. $keys = array_keys($params['named']);
  996. $named = array();
  997. for ($i = 0; $i < $count; $i++) {
  998. $named[] = $keys[$i] . $this->named['separator'] . $params['named'][$keys[$i]];
  999. }
  1000. $params['named'] = join('/', $named);
  1001. }
  1002. $params['pass'] = str_replace('//', '/', $params['pass'] . '/' . $params['named']);
  1003. }
  1004. $out = $route[0];
  1005. foreach ($route[2] as $key) {
  1006. $string = null;
  1007. if (isset($params[$key])) {
  1008. $string = $params[$key];
  1009. unset($params[$key]);
  1010. } else {
  1011. $key = $key . '/';
  1012. }
  1013. $out = str_replace(':' . $key, $string, $out);
  1014. }
  1015. if (strpos($route[0], '*')) {
  1016. $out = str_replace('*', $params['pass'], $out);
  1017. }
  1018. return $out;
  1019. }
  1020. /**
  1021. * Takes an array of URL parameters and separates the ones that can be used as named arguments
  1022. *
  1023. * @param array $params Associative array of URL parameters.
  1024. * @param string $controller Name of controller being routed. Used in scoping.
  1025. * @param string $action Name of action being routed. Used in scoping.
  1026. * @return array
  1027. * @access public
  1028. * @static
  1029. */
  1030. function getNamedElements($params, $controller = null, $action = null) {
  1031. $_this =& Router::getInstance();
  1032. $named = array();
  1033. foreach ($params as $param => $val) {
  1034. if (isset($_this->named['rules'][$param])) {
  1035. $rule = $_this->named['rules'][$param];
  1036. if (Router::matchNamed($param, $val, $rule, compact('controller', 'action'))) {
  1037. $named[$param] = $val;
  1038. unset($params[$param]);
  1039. }
  1040. }
  1041. }
  1042. return array($named, $params);
  1043. }
  1044. /**
  1045. * Return true if a given named $param's $val matches a given $rule depending on $context. Currently implemented
  1046. * rule types are controller, action and match that can be combined with each other.
  1047. *
  1048. * @param string $param The name of the named parameter
  1049. * @param string $val The value of the named parameter
  1050. * @param array $rule The rule(s) to apply, can also be a match string
  1051. * @param string $context An array with additional context information (controller / action)
  1052. * @return boolean
  1053. * @access public
  1054. */
  1055. function matchNamed($param, $val, $rule, $context = array()) {
  1056. if ($rule === true || $rule === false) {
  1057. return $rule;
  1058. }
  1059. if (is_string($rule)) {
  1060. $rule = array('match' => $rule);
  1061. }
  1062. if (!is_array($rule)) {
  1063. return false;
  1064. }
  1065. $controllerMatches = !isset($rule['controller'], $context['controller']) || in_array($context['controller'], (array)$rule['controller']);
  1066. if (!$controllerMatches) {
  1067. return false;
  1068. }
  1069. $actionMatches = !isset($rule['action'], $context['action']) || in_array($context['action'], (array)$rule['action']);
  1070. if (!$actionMatches) {
  1071. return false;
  1072. }
  1073. $valueMatches = !isset($rule['match']) || preg_match(sprintf('/%s/', $rule['match']), $val);
  1074. return $valueMatches;
  1075. }
  1076. /**
  1077. * Generates a well-formed querystring from $q
  1078. *
  1079. * @param mixed $q Query string
  1080. * @param array $extra Extra querystring parameters.
  1081. * @param bool $escape Whether or not to use escaped &
  1082. * @return array
  1083. * @access public
  1084. * @static
  1085. */
  1086. function queryString($q, $extra = array(), $escape = false) {
  1087. if (empty($q) && empty($extra)) {
  1088. return null;
  1089. }
  1090. $join = '&';
  1091. if ($escape === true) {
  1092. $join = '&amp;';
  1093. }
  1094. $out = '';
  1095. if (is_array($q)) {
  1096. $q = array_merge($extra, $q);
  1097. } else {
  1098. $out = $q;
  1099. $q = $extra;
  1100. }
  1101. $out .= http_build_query($q, null, $join);
  1102. if (isset($out[0]) && $out[0] != '?') {
  1103. $out = '?' . $out;
  1104. }
  1105. return $out;
  1106. }
  1107. /**
  1108. * Normalizes a URL for purposes of comparison
  1109. *
  1110. * @param mixed $url URL to normalize
  1111. * @return string Normalized URL
  1112. * @access public
  1113. */
  1114. function normalize($url = '/') {
  1115. if (is_array($url)) {
  1116. $url = Router::url($url);
  1117. } elseif (preg_match('/^[a-z\-]+:\/\//', $url)) {
  1118. return $url;
  1119. }
  1120. $paths = Router::getPaths();
  1121. if (!empty($paths['base']) && stristr($url, $paths['base'])) {
  1122. $url = preg_replace('/' . preg_quote($paths['base'], '/') . '/', '', $url, 1);
  1123. }
  1124. $url = '/' . $url;
  1125. while (strpos($url, '//') !== false) {
  1126. $url = str_replace('//', '/', $url);
  1127. }
  1128. $url = preg_replace('/(?:(\/$))/', '', $url);
  1129. if (empty($url)) {
  1130. return '/';
  1131. }
  1132. return $url;
  1133. }
  1134. /**
  1135. * Returns the route matching the current request URL.
  1136. *
  1137. * @return array Matching route
  1138. * @access public
  1139. * @static
  1140. */
  1141. function requestRoute() {
  1142. $_this =& Router::getInstance();
  1143. return $_this->__currentRoute[0];
  1144. }
  1145. /**
  1146. * Returns the route matching the current request (useful for requestAction traces)
  1147. *
  1148. * @return array Matching route
  1149. * @access public
  1150. * @static
  1151. */
  1152. function currentRoute() {
  1153. $_this =& Router::getInstance();
  1154. return $_this->__currentRoute[count($_this->__currentRoute) - 1];
  1155. }
  1156. /**
  1157. * Removes the plugin name from the base URL.
  1158. *
  1159. * @param string $base Base URL
  1160. * @param string $plugin Plugin name
  1161. * @return base url with plugin name removed if present
  1162. * @access public
  1163. * @static
  1164. */
  1165. function stripPlugin($base, $plugin) {
  1166. if ($plugin != null) {
  1167. $base = preg_replace('/(?:' . $plugin . ')/', '', $base);
  1168. $base = str_replace('//', '', $base);
  1169. $pos1 = strrpos($base, '/');
  1170. $char = strlen($base) - 1;
  1171. if ($pos1 === $char) {
  1172. $base = substr($base, 0, $char);
  1173. }
  1174. }
  1175. return $base;
  1176. }
  1177. /**
  1178. * Strip escape characters from parameter values.
  1179. *
  1180. * @param mixed $param Either an array, or a string
  1181. * @return mixed Array or string escaped
  1182. * @access public
  1183. * @static
  1184. */
  1185. function stripEscape($param) {
  1186. $_this =& Router::getInstance();
  1187. if (!is_array($param) || empty($param)) {
  1188. if (is_bool($param)) {
  1189. return $param;
  1190. }
  1191. $return = preg_replace('/^(?:[\\t ]*(?:-!)+)/', '', $param);
  1192. return $return;
  1193. }
  1194. foreach ($param as $key => $value) {
  1195. if (is_string($value)) {
  1196. $return[$key] = preg_replace('/^(?:[\\t ]*(?:-!)+)/', '', $value);
  1197. } else {
  1198. foreach ($value as $array => $string) {
  1199. $return[$key][$array] = $_this->stripEscape($string);
  1200. }
  1201. }
  1202. }
  1203. return $return;
  1204. }
  1205. /**
  1206. * Instructs the router to parse out file extensions from the URL. For example,
  1207. * http://example.com/posts.rss would yield an file extension of "rss".
  1208. * The file extension itself is made available in the controller as
  1209. * $this->params['url']['ext'], and is used by the RequestHandler component to
  1210. * automatically switch to alternate layouts and templates, and load helpers
  1211. * corresponding to the given content, i.e. RssHelper.
  1212. *
  1213. * A list of valid extension can be passed to this method, i.e. Router::parseExtensions('rss', 'xml');
  1214. * If no parameters are given, anything after the first . (dot) after the last / in the URL will be
  1215. * parsed, excluding querystring parameters (i.e. ?q=...).
  1216. *
  1217. * @access public
  1218. * @return void
  1219. * @static
  1220. */
  1221. function parseExtensions() {
  1222. $_this =& Router::getInstance();
  1223. $_this->__parseExtensions = true;
  1224. if (func_num_args() > 0) {
  1225. $_this->__validExtensions = func_get_args();
  1226. }
  1227. }
  1228. /**
  1229. * Takes an passed params and converts it to args
  1230. *
  1231. * @param array $params
  1232. * @return array Array containing passed and named parameters
  1233. * @access public
  1234. * @static
  1235. */
  1236. function getArgs($args, $options = array()) {
  1237. $_this =& Router::getInstance();
  1238. $pass = $named = array();
  1239. $args = explode('/', $args);
  1240. $greedy = $_this->named['greedy'];
  1241. if (isset($options['greedy'])) {
  1242. $greedy = $options['greedy'];
  1243. }
  1244. $context = array();
  1245. if (isset($options['context'])) {
  1246. $context = $options['context'];
  1247. }
  1248. $rules = $_this->named['rules'];
  1249. if (isset($options['named'])) {
  1250. $greedy = isset($options['greedy']) && $options['greedy'] === true;
  1251. foreach ((array)$options['named'] as $key => $val) {
  1252. if (is_numeric($key)) {
  1253. $rules[$val] = true;
  1254. continue;
  1255. }
  1256. $rules[$key] = $val;
  1257. }
  1258. }
  1259. foreach ($args as $param) {
  1260. if (empty($param) && $param !== '0' && $param !== 0) {
  1261. continue;
  1262. }
  1263. $param = $_this->stripEscape($param);
  1264. if ((!isset($options['named']) || !empty($options['named'])) && strpos($param, $_this->named['separator']) !== false) {
  1265. list($key, $val) = explode($_this->named['separator'], $param, 2);
  1266. $hasRule = isset($rules[$key]);
  1267. $passIt = (!$hasRule && !$greedy) || ($hasRule && !Router::matchNamed($key, $val, $rules[$key], $context));
  1268. if ($passIt) {
  1269. $pass[] = $param;
  1270. } else {
  1271. $named[$key] = $val;
  1272. }
  1273. } else {
  1274. $pass[] = $param;
  1275. }
  1276. }
  1277. return compact('pass', 'named');
  1278. }
  1279. }
  1280. ?>