diff --git a/lib/Cake/Routing/Route/Route.php b/lib/Cake/Routing/Route/Route.php index d959043acfd..6136ecfb921 100644 --- a/lib/Cake/Routing/Route/Route.php +++ b/lib/Cake/Routing/Route/Route.php @@ -88,11 +88,22 @@ class Route { 'server' => 'server_name' ); +/** + * List of connected extensions for this route. + * + * @var array + */ + protected $_extensions = array(); + /** * Constructor for a Route * - * Using $options['_name'] a specific name can be given to a route. - * Otherwise a route name will be generated. + * ### Options + * + * - `_name` - By using $options['_name'] a specific name can be + * given to a route. Otherwise a route name will be generated. + * - `_ext` - Defines the extensions used for this route. + * - `pass` - Copies the listed parameters into params['pass']. * * @param string $template Template string with parameter placeholders * @param array $defaults Array of defaults for the route. @@ -105,6 +116,9 @@ public function __construct($template, $defaults = array(), $options = array()) if (isset($this->options['_name'])) { $this->_name = $this->options['_name']; } + if (isset($this->options['_ext'])) { + $this->_extensions = $this->options['_ext']; + } } /** @@ -225,6 +239,8 @@ public function parse($url) { if (!$this->compiled()) { $this->compile(); } + list($url, $ext) = $this->_parseExtension($url); + if (!preg_match($this->_compiledRoute, $url, $route)) { return false; } @@ -287,6 +303,10 @@ public function parse($url) { unset($route['_trailing_']); } + if ($ext && empty($route['_ext'])) { + $route['_ext'] = $ext; + } + // restructure 'pass' key route params if (isset($this->options['pass'])) { $j = count($this->options['pass']); @@ -299,6 +319,32 @@ public function parse($url) { return $route; } +/** + * Removes the extension if the $url contains a known extension. + * If there are no known extensions all extensions are supported. + * + * @param string $url The url to parse. + * @return array containing url, extension + */ + protected function _parseExtension($url) { + if (empty($this->_extensions)) { + return array($url, null); + } + preg_match('/\.([0-9a-z]*)$/', $url, $match); + if (empty($match[1])) { + return array($url, null); + } + $ext = strtolower($match[1]); + $len = strlen($match[1]); + foreach ($this->_extensions as $name) { + if (strtolower($name) === $ext) { + $url = substr($url, 0, ($len + 1) * -1); + return array($url, $ext); + } + } + return array($url, null); + } + /** * Parse passed parameters into a list of passed args. * diff --git a/lib/Cake/Routing/Router.php b/lib/Cake/Routing/Router.php index 7a544f4f01f..4923c313e9c 100644 --- a/lib/Cake/Routing/Router.php +++ b/lib/Cake/Routing/Router.php @@ -218,20 +218,23 @@ public static function resourceMap($resourceMap = null) { /** * Connects a new Route in the router. * - * Routes are a way of connecting request urls to objects in your application. At their core routes - * are a set or regular expressions that are used to match requests to destinations. + * Routes are a way of connecting request urls to objects in your application. + * At their core routes are a set or regular expressions that are used to + * match requests to destinations. * * Examples: * * `Router::connect('/:controller/:action/*');` * - * The first parameter will be used as a controller name while the second is used as the action name. - * the '/*' syntax makes this route greedy in that it will match requests like `/posts/index` as well as requests + * The first parameter will be used as a controller name while the second is + * used as the action name. The '/*' syntax makes this route greedy in that + * it will match requests like `/posts/index` as well as requests * like `/posts/edit/1/foo/bar`. * * `Router::connect('/home-page', array('controller' => 'pages', 'action' => 'display', 'home'));` * - * The above shows the use of route parameter defaults. And providing routing parameters for a static route. + * The above shows the use of route parameter defaults. And providing routing + * parameters for a static route. * * {{{ * Router::connect( @@ -241,17 +244,22 @@ public static function resourceMap($resourceMap = null) { * ); * }}} * - * Shows connecting a route with custom route parameters as well as providing patterns for those parameters. - * Patterns for routing parameters do not need capturing groups, as one will be added for each route params. + * Shows connecting a route with custom route parameters as well as + * providing patterns for those parameters. Patterns for routing parameters + * do not need capturing groups, as one will be added for each route params. * - * $options offers several 'special' keys that have special meaning in the $options array. + * $options offers several 'special' keys that have special meaning + * in the $options array. * - * - `pass` is used to define which of the routed parameters should be shifted into the pass array. Adding a - * parameter to pass will remove it from the regular route array. Ex. `'pass' => array('slug')` - * - `routeClass` is used to extend and change how individual routes parse requests and handle reverse routing, - * via a custom routing class. Ex. `'routeClass' => 'SlugRoute'` - * - `_name` Used to define a specific name for routes. This can be used to optimize reverse routing lookups. - * If undefined a name will be generated for each connected route. + * - `pass` is used to define which of the routed parameters should be shifted + * into the pass array. Adding a parameter to pass will remove it from the + * regular route array. Ex. `'pass' => array('slug')`. + * - `routeClass` is used to extend and change how individual routes parse requests + * and handle reverse routing, via a custom routing class. + * Ex. `'routeClass' => 'SlugRoute'` + * - `_name` Used to define a specific name for routes. This can be used to optimize + * reverse routing lookups. If undefined a name will be generated for each + * connected route. * * @param string $route A string describing the template of the route * @param array $defaults An array describing the default route parameters. These parameters will be used by default @@ -283,6 +291,9 @@ public static function connect($route, $defaults = array(), $options = array()) if (empty($options['action'])) { $defaults += array('action' => 'index'); } + if (empty($options['_ext'])) { + $options['_ext'] = self::$_validExtensions; + } $routeClass = static::$_routeClass; if (isset($options['routeClass'])) { $routeClass = static::_validateRouteClass($options['routeClass']); @@ -412,21 +423,23 @@ public static function parse($url) { $url = substr($url, 0, strpos($url, '?')); } +<<<<<<< HEAD extract(static::_parseExtension($url)); $out = static::$_routes->parse($url); +======= + $out = self::$_routes->parse($url); +>>>>>>> Move extension parsing. if (isset($out['prefix'])) { $out['action'] = $out['prefix'] . '_' . $out['action']; } - if (!empty($ext) && !isset($out['ext'])) { - $out['ext'] = $ext; - } return $out; } /** +<<<<<<< HEAD * Parses a file extension out of a URL, if Router::parseExtensions() is enabled. * * @param string $url @@ -456,6 +469,8 @@ protected static function _parseExtension($url) { } /** +======= +>>>>>>> Move extension parsing. * Set the route collection object Router should use. * * @param Cake\Routing\RouteCollection $routes @@ -890,7 +905,7 @@ public static function normalize($url = '/') { * Instructs the router to parse out file extensions from the URL. For example, * http://example.com/posts.rss would yield an file extension of "rss". * The file extension itself is made available in the controller as - * `$this->params['ext']`, and is used by the RequestHandler component to + * `$this->params['_ext']`, and is used by the RequestHandler component to * automatically switch to alternate layouts and templates, and load helpers * corresponding to the given content, i.e. RssHelper. Switching layouts and helpers * requires that the chosen extension has a defined mime type in `Cake\Network\Response` diff --git a/lib/Cake/Test/TestCase/Routing/Route/RouteTest.php b/lib/Cake/Test/TestCase/Routing/Route/RouteTest.php index 9e251a42fe5..55dcb317434 100644 --- a/lib/Cake/Test/TestCase/Routing/Route/RouteTest.php +++ b/lib/Cake/Test/TestCase/Routing/Route/RouteTest.php @@ -92,6 +92,31 @@ public function testBasicRouteCompiling() { $this->assertRegExp($result, '/test_plugin/posts/edit/5/name:value/nick:name'); } +/** + * Test parsing routes with extensions. + * + * @return void + */ + public function testRouteParsingWithExtensions() { + $route = new Route( + '/:controller/:action/*', + array(), + array('_ext' => array('json', 'xml')) + ); + + $result = $route->parse('/posts/index'); + $this->assertFalse(isset($result['_ext'])); + + $result = $route->parse('/posts/index.pdf'); + $this->assertFalse(isset($result['_ext'])); + + $result = $route->parse('/posts/index.json'); + $this->assertEquals('json', $result['_ext']); + + $result = $route->parse('/posts/index.xml'); + $this->assertEquals('xml', $result['_ext']); + } + /** * test that route parameters that overlap don't cause errors. * diff --git a/lib/Cake/Test/TestCase/Routing/RouterTest.php b/lib/Cake/Test/TestCase/Routing/RouterTest.php index 214aa5610c1..c956cdee0eb 100644 --- a/lib/Cake/Test/TestCase/Routing/RouterTest.php +++ b/lib/Cake/Test/TestCase/Routing/RouterTest.php @@ -573,7 +573,7 @@ public function testUrlGenerationWithAdminPrefix() { 'plugin' => null, 'prefix' => 'admin', 'admin' => true, - 'ext' => 'html' + '_ext' => 'html' )); $request->base = ''; $request->here = '/admin/registrations/index'; @@ -747,15 +747,15 @@ public function testUrlGenerationWithExtensions() { Router::connect('/:controller/:action'); Router::parse('/'); - $result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', 'id' => null, 'ext' => 'json')); + $result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', 'id' => null, '_ext' => 'json')); $expected = '/articles/add.json'; $this->assertEquals($expected, $result); - $result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', 'ext' => 'json')); + $result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', '_ext' => 'json')); $expected = '/articles/add.json'; $this->assertEquals($expected, $result); - $result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'index', 'id' => null, 'ext' => 'json')); + $result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'index', 'id' => null, '_ext' => 'json')); $expected = '/articles.json'; $this->assertEquals($expected, $result); @@ -1212,48 +1212,85 @@ public function testSetExtensions() { * @return void */ public function testExtensionParsing() { + /* Router::parseExtensions(); require CAKE . 'Config' . DS . 'routes.php'; $result = Router::parse('/posts.rss'); - $expected = array('plugin' => null, 'controller' => 'posts', 'action' => 'index', 'ext' => 'rss', 'pass' => array()); + $expected = array( + 'plugin' => null, + 'controller' => 'posts', + 'action' => 'index', + '_ext' => 'rss', + 'pass' => array() + ); $this->assertEquals($expected, $result); $result = Router::parse('/posts/view/1.rss'); - $expected = array('plugin' => null, 'controller' => 'posts', 'action' => 'view', 'pass' => array('1'), 'ext' => 'rss'); + $expected = array( + 'plugin' => null, + 'controller' => 'posts', + 'action' => 'view', + 'pass' => array('1'), + '_ext' => 'rss' + ); $this->assertEquals($expected, $result); $result = Router::parse('/posts/view/1.rss?query=test'); $this->assertEquals($expected, $result); $result = Router::parse('/posts/view/1.atom'); - $expected['ext'] = 'atom'; + $expected['_ext'] = 'atom'; $this->assertEquals($expected, $result); - + */ Router::reload(); + Router::parseExtensions('rss', 'xml'); + require CAKE . 'Config' . DS . 'routes.php'; - Router::parseExtensions('rss', 'xml'); $result = Router::parse('/posts.xml'); - $expected = array('plugin' => null, 'controller' => 'posts', 'action' => 'index', 'ext' => 'xml', 'pass' => array()); + $expected = array( + 'plugin' => null, + 'controller' => 'posts', + 'action' => 'index', + '_ext' => 'xml', + 'pass' => array() + ); $this->assertEquals($expected, $result); $result = Router::parse('/posts.atom?hello=goodbye'); - $expected = array('plugin' => null, 'controller' => 'posts.atom', 'action' => 'index', 'pass' => array()); + $expected = array( + 'plugin' => null, + 'controller' => 'posts.atom', + 'action' => 'index', + 'pass' => array() + ); $this->assertEquals($expected, $result); Router::reload(); - Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', 'ext' => 'rss')); + Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', '_ext' => 'rss')); $result = Router::parse('/controller/action'); - $expected = array('controller' => 'controller', 'action' => 'action', 'plugin' => null, 'ext' => 'rss', 'pass' => array()); + $expected = array( + 'controller' => 'controller', + 'action' => 'action', + 'plugin' => null, + '_ext' => 'rss', + 'pass' => array() + ); $this->assertEquals($expected, $result); Router::reload(); Router::parseExtensions('rss'); - Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', 'ext' => 'rss')); + Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', '_ext' => 'rss')); $result = Router::parse('/controller/action'); - $expected = array('controller' => 'controller', 'action' => 'action', 'plugin' => null, 'ext' => 'rss', 'pass' => array()); + $expected = array( + 'controller' => 'controller', + 'action' => 'action', + 'plugin' => null, + '_ext' => 'rss', + 'pass' => array() + ); $this->assertEquals($expected, $result); }