Cake/Routing/Router.php

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