From 847811701c1e78873e1e0b9691ea1577a1458160 Mon Sep 17 00:00:00 2001 From: the_undefined Date: Sun, 18 May 2008 22:52:28 +0000 Subject: [PATCH] Fixed Router::connectNamed, added enhanced named parameter control, closes #4451 git-svn-id: https://svn.cakephp.org/repo/branches/1.2.x.x@6933 3807eeeb-6ff5-0310-8944-8be069107fe0 --- cake/libs/router.php | 158 +++++++++++++++++--------- cake/tests/cases/libs/router.test.php | 149 ++++++++++++++++-------- 2 files changed, 206 insertions(+), 101 deletions(-) diff --git a/cake/libs/router.php b/cake/libs/router.php index 35c9ad9ba..fbc3381aa 100644 --- a/cake/libs/router.php +++ b/cake/libs/router.php @@ -90,6 +90,18 @@ class Router extends Object { 'ID' => '[0-9]+', '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}' ); +/** + * Stores all information necessary to decide what named arguments are parsed under what conditions. + * + * @var string + * @access public + */ + var $named = array( + 'default' => array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step'), + 'greedy' => true, + 'separator' => ':', + 'rules' => false, + ); /** * The route matching the URL of the current request * @@ -136,20 +148,6 @@ class Router extends Object { * @access private */ var $__params = array(); -/** - * List of named arguments allowed in routes - * - * @var array - * @access private - */ - var $__namedArgs = array(); -/** - * Separator used to join/split/detect named arguments - * - * @var string - * @access private - */ - var $__argSeparator = ':'; /** * Maintains the path stack for the current request * @@ -231,18 +229,31 @@ class Router extends Object { */ function connectNamed($named, $options = array()) { $_this =& Router::getInstance(); - if (isset($options['argSeparator'])) { - $_this->__argSeparator = $options['argSeparator']; + $options['separator'] = $options['argSeparator']; + unset($options['argSeparator']); + } + if ($named === true || $named === false) { + $options = array('default' => $named, 'reset' => true, 'greedy' => $named); + $named = array(); + } + $options = array_merge(array('default' => false, 'reset' => false, 'greedy' => true), $options); + if ($options['reset'] == true || $_this->named['rules'] === false) { + $_this->named['rules'] = array(); + } + if ($options['default']) { + $named = array_merge($named, $_this->named['default']); } foreach ($named as $key => $val) { if (is_numeric($key)) { - $_this->__namedArgs[$val] = true; + $_this->named['rules'][$val] = true; } else { - $_this->__namedArgs[$key] = $val; + $_this->named['rules'][$key] = $val; } } + $_this->named['greedy'] = $options['greedy']; + return $_this->named; } /** * Creates REST resource routes for the given controller(s) @@ -386,10 +397,14 @@ class Router extends Object { $_this->__currentRoute[] = $route; list($route, $regexp, $names, $defaults, $params) = $route; $argOptions = array(); - if (isset($params['named'])) { - $argOptions = array('named' => $params['named']); + if (array_key_exists('named', $params)) { + $argOptions['named'] = $params['named']; unset($params['named']); } + if (array_key_exists('greedy', $params)) { + $argOptions['greedy'] = $params['greedy']; + unset($params['greedy']); + } // remove the first element, which is the url array_shift($r); // hack, pre-fill the default route names @@ -406,6 +421,7 @@ class Router extends Object { } } } + foreach ($r as $key => $found) { if (empty($found)) { continue; @@ -416,6 +432,7 @@ class Router extends Object { } elseif (isset($names[$key]) && empty($names[$key]) && empty($out[$names[$key]])) { break; //leave the default values; } else { + $argOptions['context'] = array('action' => $out['action'], 'controller' => $out['controller']); extract($_this->getArgs($found, $argOptions)); $out['pass'] = array_merge($out['pass'], $pass); $out['named'] = $named; @@ -549,8 +566,8 @@ class Router extends Object { $_this->connect('/:controller', array('action' => 'index')); $_this->connect('/:controller/:action/*'); - if (empty($_this->__namedArgs)) { - $_this->connectNamed(array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step')); + if ($_this->named['rules'] === false) { + $_this->connectNamed(true); } $_this->__defaultsMapped = true; } @@ -571,7 +588,7 @@ class Router extends Object { if (count($_this->__paths)) { if (isset($_this->__paths[0]['namedArgs'])) { foreach ($_this->__paths[0]['namedArgs'] as $arg => $value) { - $_this->__namedArgs[$arg] = true; + $_this->named['rules'][$arg] = true; } } } @@ -815,7 +832,7 @@ class Router extends Object { if (!empty($named)) { foreach ($named as $name => $value) { - $output .= '/' . $name . $_this->__argSeparator . $value; + $output .= '/' . $name . $_this->named['separator'] . $value; } } @@ -990,7 +1007,7 @@ class Router extends Object { $named = array(); for ($i = 0; $i < $count; $i++) { - $named[] = $keys[$i] . $_this->__argSeparator . $params['named'][$keys[$i]]; + $named[] = $keys[$i] . $_this->named['separator'] . $params['named'][$keys[$i]]; } $params['named'] = join('/', $named); } @@ -1026,33 +1043,50 @@ class Router extends Object { $_this =& Router::getInstance(); $named = array(); - foreach ($params as $key => $val) { - if (isset($_this->__namedArgs[$key])) { - $match = true; - - if (is_array($_this->__namedArgs[$key])) { - $opts = $_this->__namedArgs[$key]; - if (isset($opts['controller']) && !in_array($controller, (array)$opts['controller'])) { - $match = false; - } - if (isset($opts['action']) && !in_array($action, (array)$opts['action'])) { - $match = false; - } - if (isset($opts['match']) && !preg_match('/' . $opts['match'] . '/', $val)) { - $match = false; - } - } elseif (!$_this->__namedArgs[$key]) { - $match = false; - } - if ($match) { - $named[$key] = $val; - unset($params[$key]); + foreach ($params as $param => $val) { + if (isset($_this->named['rules'][$param])) { + $rule = $_this->named['rules'][$param]; + if (Router::matchNamed($param, $val, $rule, compact('controller', 'action'))) { + $named[$param] = $val; + unset($params[$param]); } } } return array($named, $params); } +/** + * Return true if a given named $param's $val matches a given $rule depending on $context. Currently implemented + * rule types are controller, action and match that can be combined with each other. + * + * @param string $param The name of the named parameter + * @param string $val The value of the named parameter + * @param array $rule The rule(s) to apply, can also be a match string + * @param string $context An array with additional context information (controller / action) + * @return boolean + * @access public + */ + function matchNamed($param, $val, $rule, $context = array()) { + if ($rule === true || $rule === false) { + return $rule; + } + if (is_string($rule)) { + $rule = array('match' => $rule); + } + if (!is_array($rule)) { + return false; + } + $controllerMatches = !isset($rule['controller'], $context['controller']) || in_array($context['controller'], (array)$rule['controller']); + if (!$controllerMatches) { + return false; + } + $actionMatches = !isset($rule['action'], $context['action']) || in_array($context['action'], (array)$rule['action']); + if (!$actionMatches) { + return false; + } + $valueMatches = !isset($rule['match']) || preg_match(sprintf('/%s/', $rule['match']), $val); + return $valueMatches; + } /** * Generates a well-formed querystring from $q * @@ -1204,7 +1238,6 @@ class Router extends Object { $_this->__validExtensions = func_get_args(); } } - /** * Takes an passed params and converts it to args * @@ -1216,19 +1249,42 @@ class Router extends Object { $_this =& Router::getInstance(); $pass = $named = array(); $args = explode('/', $args); + + $greedy = $_this->named['greedy']; + if (isset($options['greedy'])) { + $greedy = $options['greedy']; + } + $context = array(); + if (isset($options['context'])) { + $context = $options['context']; + } + $rules = $_this->named['rules']; + if (isset($options['named'])) { + $greedy = isset($options['greedy']) && $options['greedy'] == true; + foreach ((array)$options['named'] as $key => $val) { + if (is_numeric($key)) { + $rules[$val] = true; + continue; + } + $rules[$key] = $val; + } + } + foreach ($args as $param) { if (empty($param) && $param !== '0' && $param !== 0) { continue; } $param = $_this->stripEscape($param); - if ((!isset($options['named']) || !empty($options['named'])) && strpos($param, $_this->__argSeparator)) { - list($key, $val) = explode($_this->__argSeparator, $param, 2); - if (isset($options['named']) && is_array($options['named']) && !in_array($key, $options['named'])) { + if ((!isset($options['named']) || !empty($options['named'])) && strpos($param, $_this->named['separator'])) { + list($key, $val) = explode($_this->named['separator'], $param, 2); + + $hasRule = isset($rules[$key]); + $passIt = (!$hasRule && !$greedy) || ($hasRule && !Router::matchNamed($key, $val, $rules[$key], $context)); + if ($passIt) { $pass[] = $param; } else { $named[$key] = $val; } - } else { $pass[] = $param; } diff --git a/cake/tests/cases/libs/router.test.php b/cake/tests/cases/libs/router.test.php index 96dc183e9..5af4fb076 100644 --- a/cake/tests/cases/libs/router.test.php +++ b/cake/tests/cases/libs/router.test.php @@ -12,8 +12,8 @@ * 1785 E. Sahara Avenue, Suite 490-204 * Las Vegas, Nevada 89104 * - * Licensed under The Open Group Test Suite License - * Redistributions of files must retain the above copyright notice. + * Licensed under The Open Group Test Suite License + * Redistributions of files must retain the above copyright notice. * * @filesource * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. @@ -261,7 +261,7 @@ class RouterTest extends UnitTestCase { $expected = '/posts/index/0?var=test&var2=test2#unencoded+string+%25'; $this->assertEqual($result, $expected); - Router::connect('/view/*', array('controller' => 'posts', 'action' => 'view')); + Router::connect('/view/*', array('controller' => 'posts', 'action' => 'view')); Router::promote(); $result = Router::url(array('controller' => 'posts', 'action' => 'view', '1')); $expected = '/view/1'; @@ -272,7 +272,7 @@ class RouterTest extends UnitTestCase { Router::setRequestInfo(array( array( 'pass' => array(), 'action' => 'admin_index', 'plugin' => null, 'controller' => 'subscriptions', - 'admin' => true, 'url' => array('url' => 'admin/subscriptions/index/page:2'), + 'admin' => true, 'url' => array('url' => 'admin/subscriptions/index/page:2'), ), array( 'base' => '/magazine', 'here' => '/magazine/admin/subscriptions/index/page:2', @@ -291,7 +291,7 @@ class RouterTest extends UnitTestCase { Router::setRequestInfo(array( array( 'pass' => array(), 'action' => 'admin_index', 'plugin' => null, 'controller' => 'subscribe', - 'admin' => true, 'url' => array('url' => 'admin/subscriptions/edit/1') + 'admin' => true, 'url' => array('url' => 'admin/subscriptions/edit/1') ), array( 'base' => '/magazine', 'here' => '/magazine/admin/subscriptions/edit/1', @@ -372,7 +372,7 @@ class RouterTest extends UnitTestCase { $expected = '/eng/pages/add'; $this->assertEqual($result, $expected); - Router::reload(); + Router::reload(); Router::parse('/'); Router::setRequestInfo(array( array('pass' => array(), 'action' => 'index', 'plugin' => null, 'controller' => 'users', 'url' => array('url' => 'users')), @@ -463,7 +463,7 @@ class RouterTest extends UnitTestCase { Router::setRequestInfo(array( array( 'pass' => array(), 'action' => 'index', 'plugin' => 'myplugin', 'controller' => 'mycontroller', - 'admin' => false, 'url' => array('url' => array()) + 'admin' => false, 'url' => array('url' => array()) ), array( 'base' => '/', 'here' => '/', @@ -701,9 +701,9 @@ class RouterTest extends UnitTestCase { $this->assertEqual($result, $expected); Router::reload(); - Router::connect('/posts/view/*', array('controller' => 'posts', 'action' => 'view'), array('named' => array('foo', 'answer'))); + Router::connect('/posts/view/*', array('controller' => 'posts', 'action' => 'view'), array('named' => array('foo', 'answer'), 'greedy' => true)); $result = Router::parse('/posts/view/foo:bar/routing:fun/answer:42'); - $expected = array('pass' => array('routing:fun'), 'named' => array('foo' => 'bar', 'answer' => '42'), 'plugin' => null, 'controller' => 'posts', 'action' => 'view'); + $expected = array('pass' => array(), 'named' => array('foo' => 'bar', 'routing' => 'fun', 'answer' => '42'), 'plugin' => null, 'controller' => 'posts', 'action' => 'view'); $this->assertEqual($result, $expected); Router::reload(); @@ -739,20 +739,20 @@ class RouterTest extends UnitTestCase { function testUuidRoutes() { Router::connect( - '/subjects/add/:category_id', - array('controller' => 'subjects', 'action' => 'add'), - array('category_id' => '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}') + '/subjects/add/:category_id', + array('controller' => 'subjects', 'action' => 'add'), + array('category_id' => '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}') ); - $result = Router::parse('/subjects/add/4795d601-19c8-49a6-930e-06a8b01d17b7'); + $result = Router::parse('/subjects/add/4795d601-19c8-49a6-930e-06a8b01d17b7'); $expected = array('pass' => array(), 'named' => array(), 'category_id' => '4795d601-19c8-49a6-930e-06a8b01d17b7', 'plugin' => null, 'controller' => 'subjects', 'action' => 'add'); $this->assertEqual($result, $expected); } function testRouteSymmetry() { Router::connect( - "/:extra/page/:slug/*", - array('controller' => 'pages', 'action' => 'view', 'extra' => null), - array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+', "action" => 'view') + "/:extra/page/:slug/*", + array('controller' => 'pages', 'action' => 'view', 'extra' => null), + array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+', "action" => 'view') ); $result = Router::parse('/some_extra/page/this_is_the_slug'); @@ -766,9 +766,9 @@ class RouterTest extends UnitTestCase { Router::reload(); Router::connect( - "/:extra/page/:slug/*", - array('controller' => 'pages', 'action' => 'view', 'extra' => null), - array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+') + "/:extra/page/:slug/*", + array('controller' => 'pages', 'action' => 'view', 'extra' => null), + array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+') ); Router::parse('/'); @@ -906,13 +906,6 @@ class RouterTest extends UnitTestCase { } function testNamedArgsUrlGeneration() { - Router::setRequestInfo(array(null, array('base' => '/', 'argSeparator' => ':'))); - Router::connectNamed(array( - 'published' => array('regex' => '[01]'), - 'deleted' => array('regex' => '[01]') - )); - Router::parse('/'); - $result = Router::url(array('controller' => 'posts', 'action' => 'index', 'published' => 1, 'deleted' => 1)); $expected = '/posts/index/published:1/deleted:1'; $this->assertEqual($result, $expected); @@ -923,7 +916,6 @@ class RouterTest extends UnitTestCase { Router::reload(); extract(Router::getNamedExpressions()); - Router::setRequestInfo(array(null, array('base' => '/', 'argSeparator' => ':'))); Router::connectNamed(array('file'=> '[\w\.\-]+\.(html|png)')); Router::connect('/', array('controller' => 'graphs', 'action' => 'index')); Router::connect('/:id/*', array('controller' => 'graphs', 'action' => 'view'), array('id' => $ID)); @@ -932,6 +924,10 @@ class RouterTest extends UnitTestCase { $expected = '/12/file:asdf.png'; $this->assertEqual($result, $expected); + $result = Router::url(array('controller' => 'graphs', 'action' => 'view', 'id' => 12, 'file' => 'asdf.foo')); + $expected = '/graphs/view/12/file:asdf.foo'; + $this->assertEqual($result, $expected); + Configure::write('Routing.admin', 'admin'); Router::reload(); @@ -951,42 +947,95 @@ class RouterTest extends UnitTestCase { array('base' => '/', 'here' => '/', 'webroot' => '/base/', 'passedArgs' => array('type'=> 'whatever'), 'argSeparator' => ':', 'namedArgs' => array('type'=> 'whatever')) )); - Router::connectNamed(array('type')); - - Router::parse('/admin/controller/index/type:whatever'); - + $result = Router::parse('/admin/controller/index/type:whatever'); $result = Router::url(array('type'=> 'new')); $expected = "/admin/controller/index/type:new"; $this->assertEqual($result, $expected); } function testNamedArgsUrlParsing() { + $Router =& Router::getInstance(); + Router::reload(); $result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param:value'); $expected = array('pass' => array(), 'named' => array('param1' => 'value1:1', 'param2' => 'value2:3', 'param' => 'value'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null); $this->assertEqual($result, $expected); + + Router::reload(); + $result = Router::connectNamed(false); + $this->assertEqual(array_keys($result['rules']), array()); + $this->assertFalse($result['greedy']); + $result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param:value'); + $expected = array('pass' => array('param1:value1:1', 'param2:value2:3', 'param:value'), 'named' => array(), 'controller' => 'controller', 'action' => 'action', 'plugin' => null); + $this->assertEqual($result, $expected); + + Router::reload(); + $result = Router::connectNamed(true); + $this->assertEqual(array_keys($result['rules']), $Router->named['default']); + $this->assertTrue($result['greedy']); + Router::reload(); + Router::connectNamed(array('param1' => 'not-matching')); + $result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param:value'); + $expected = array('pass' => array('param1:value1:1'), 'named' => array('param2' => 'value2:3', 'param' => 'value'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null); + $this->assertEqual($result, $expected); + + Router::reload(); + Router::connect('/foo/:action/*', array('controller' => 'bar'), array('named' => array('param1' => array('action' => 'index')), 'greedy' => true)); + $result = Router::parse('/foo/index/param1:value1:1/param2:value2:3/param:value'); + $expected = array('pass' => array(), 'named' => array('param1' => 'value1:1', 'param2' => 'value2:3', 'param' => 'value'), 'controller' => 'bar', 'action' => 'index', 'plugin' => null); + $this->assertEqual($result, $expected); + + $result = Router::parse('/foo/view/param1:value1:1/param2:value2:3/param:value'); + $expected = array('pass' => array('param1:value1:1'), 'named' => array('param2' => 'value2:3', 'param' => 'value'), 'controller' => 'bar', 'action' => 'view', 'plugin' => null); + $this->assertEqual($result, $expected); + + Router::reload(); + Router::connectNamed(array('param1' => '[\d]', 'param2' => '[a-z]', 'param3' => '[\d]')); + $result = Router::parse('/controller/action/param1:1/param2:2/param3:3'); + $expected = array('pass' => array('param2:2'), 'named' => array('param1' => '1', 'param3' => '3'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null); + $this->assertEqual($result, $expected); + + Router::reload(); + Router::connectNamed(array('param1' => '[\d]', 'param2' => true, 'param3' => '[\d]')); + $result = Router::parse('/controller/action/param1:1/param2:2/param3:3'); + $expected = array('pass' => array(), 'named' => array('param1' => '1', 'param2' => '2', 'param3' => '3'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null); + $this->assertEqual($result, $expected); + + Router::reload(); + Router::connectNamed(array('param1' => 'value[\d]+:[\d]+'), array('greedy' => false)); + $result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param3:value'); + $expected = array('pass' => array('param2:value2:3', 'param3:value'), 'named' => array('param1' => 'value1:1'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null); + $this->assertEqual($result, $expected); + + Router::reload(); + Router::connect('/foo/*', array('controller' => 'bar', 'action' => 'fubar'), array('named' => array('param1' => 'value[\d]:[\d]'))); + Router::connectNamed(array(), array('greedy' => false)); + $result = Router::parse('/foo/param1:value1:1/param2:value2:3/param3:value'); + $expected = array('pass' => array('param2:value2:3', 'param3:value'), 'named' => array('param1' => 'value1:1'), 'controller' => 'bar', 'action' => 'fubar', 'plugin' => null); + $this->assertEqual($result, $expected); } function testUrlGenerationWithPrefixes() { + Router::reload(); Router::connect('/protected/:controller/:action/*', array( - 'controller' => 'users', - 'action' => 'index', - 'prefix' => 'protected', + 'controller' => 'users', + 'action' => 'index', + 'prefix' => 'protected', 'protected' => true )); Router::parse('/'); - Router::setRequestInfo(array( - array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'prefix' => null, 'admin' => false, 'form' => array(), 'url' => array('url' => 'images/index')), + Router::setRequestInfo(array( + array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'prefix' => null, 'admin' => false, 'form' => array(), 'url' => array('url' => 'images/index')), array('plugin' => null, 'controller' => null, 'action' => null, 'base' => '', 'here' => '/images/index', 'webroot' => '/') )); - $result = Router::url(array('controller' => 'images', 'action' => 'add')); - $expected = '/images/add'; - $this->assertEqual($result, $expected); + $result = Router::url(array('controller' => 'images', 'action' => 'add')); + $expected = '/images/add'; + $this->assertEqual($result, $expected); - $result = Router::url(array('controller' => 'images', 'action' => 'add', 'protected' => true)); - $expected = '/protected/images/add'; - $this->assertEqual($result, $expected); + $result = Router::url(array('controller' => 'images', 'action' => 'add', 'protected' => true)); + $expected = '/protected/images/add'; + $this->assertEqual($result, $expected); } function testRemoveBase() { @@ -1129,21 +1178,21 @@ class RouterTest extends UnitTestCase { Router::reload(); - Router::setRequestInfo(array( - array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'named' => array(), 'prefix' => 'protected', 'admin' => false, 'form' => array(), 'url' => array ('url' => 'protected/images/index')), + Router::setRequestInfo(array( + array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'named' => array(), 'prefix' => 'protected', 'admin' => false, 'form' => array(), 'url' => array ('url' => 'protected/images/index')), array('plugin' => null, 'controller' => null, 'action' => null, 'base' => '', 'here' => '/protected/images/index', 'webroot' => '/') )); Router::connect('/protected/:controller/:action/*', array( - 'controller' => 'users', - 'action' => 'index', - 'prefix' => 'protected' + 'controller' => 'users', + 'action' => 'index', + 'prefix' => 'protected' )); Router::parse('/'); - $result = Router::url(array('controller' => 'images', 'action' => 'add')); - $expected = '/protected/images/add'; - $this->assertEqual($result, $expected); + $result = Router::url(array('controller' => 'images', 'action' => 'add')); + $expected = '/protected/images/add'; + $this->assertEqual($result, $expected); $result = Router::prefixes(); $expected = array('protected', 'admin');