PageRenderTime 62ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/cake/libs/router.php

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