[Routing] fixed possible parameters conflict in apache url matcher

This commit is contained in:
Arnaud Le Blanc 2012-10-19 22:37:28 +02:00
parent 390f36a86b
commit c7a8f7af62
5 changed files with 183 additions and 122 deletions

View File

@ -17,6 +17,7 @@ use Symfony\Component\Routing\Exception\MethodNotAllowedException;
* ApacheUrlMatcher matches URL based on Apache mod_rewrite matching (see ApacheMatcherDumper).
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
*/
class ApacheUrlMatcher extends UrlMatcher
{
@ -36,36 +37,52 @@ class ApacheUrlMatcher extends UrlMatcher
$parameters = array();
$defaults = array();
$allow = array();
$match = false;
$route = null;
foreach ($_SERVER as $key => $value) {
$name = $key;
if (0 === strpos($name, 'REDIRECT_')) {
// skip non-routing variables
// this improves performance when $_SERVER contains many usual
// variables like HTTP_*, DOCUMENT_ROOT, REQUEST_URI, ...
if (false === strpos($name, '_ROUTING_')) {
continue;
}
while (0 === strpos($name, 'REDIRECT_')) {
$name = substr($name, 9);
}
if (0 === strpos($name, '_ROUTING_DEFAULTS_')) {
$name = substr($name, 18);
$defaults[$name] = $value;
} elseif (0 === strpos($name, '_ROUTING_')) {
$name = substr($name, 9);
if ('_route' == $name) {
$match = true;
$parameters[$name] = $value;
} elseif (0 === strpos($name, '_allow_')) {
$allow[] = substr($name, 7);
} else {
// expect _ROUTING_<type>_<name>
// or _ROUTING_<type>
if (0 !== strpos($name, '_ROUTING_')) {
continue;
}
if (false !== $pos = strpos($name, '_', 9)) {
$type = substr($name, 9, $pos-9);
$name = substr($name, $pos+1);
} else {
$type = substr($name, 9);
}
if ('param' === $type) {
if ('' !== $value) {
$parameters[$name] = $value;
}
} else {
continue;
} elseif ('default' === $type) {
$defaults[$name] = $value;
} elseif ('route' === $type) {
$route = $value;
} elseif ('allow' === $type) {
$allow[] = $name;
}
unset($_SERVER[$key]);
}
if ($match) {
if (null !== $route) {
$parameters['_route'] = $route;
return $this->mergeDefaults($parameters, $defaults);
} elseif (0 < count($allow)) {
throw new MethodNotAllowedException($allow);

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Routing\Matcher\Dumper;
use Symfony\Component\Routing\Route;
/**
* Dumps a set of Apache mod_rewrite rules.
*
@ -46,81 +48,15 @@ class ApacheMatcherDumper extends MatcherDumper
$methodVars = array();
foreach ($this->getRoutes()->all() as $name => $route) {
$compiledRoute = $route->compile();
// prepare the apache regex
$regex = $compiledRoute->getRegex();
$delimiter = $regex[0];
$regexPatternEnd = strrpos($regex, $delimiter);
if (strlen($regex) < 2 || 0 === $regexPatternEnd) {
throw new \LogicException('The "%s" route regex "%s" is invalid', $name, $regex);
}
$regex = preg_replace('/\?<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1));
$regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\');
$methods = array();
if ($req = $route->getRequirement('_method')) {
$methods = explode('|', strtoupper($req));
// GET and HEAD are equivalent
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
$methods[] = 'HEAD';
}
}
$hasTrailingSlash = (!$methods || in_array('HEAD', $methods)) && '/$' === substr($regex, -2) && '^/$' !== $regex;
$variables = array('E=_ROUTING__route:'.$name);
foreach ($compiledRoute->getVariables() as $i => $variable) {
$variables[] = 'E=_ROUTING_'.$variable.':%'.($i + 1);
}
foreach ($route->getDefaults() as $key => $value) {
$variables[] = 'E=_ROUTING_DEFAULTS_'.$key.':'.strtr($value, array(
':' => '\\:',
'=' => '\\=',
'\\' => '\\\\',
' ' => '\\ ',
));
}
$variables = implode(',', $variables);
$rule = array("# $name");
// method mismatch
if ($req = $route->getRequirement('_method')) {
$methods = explode('|', strtoupper($req));
// GET and HEAD are equivalent
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
$methods[] = 'HEAD';
}
$allow = array();
foreach ($methods as $method) {
$methodVars[] = $method;
$allow[] = 'E=_ROUTING__allow_'.$method.':1';
}
$rule[] = "RewriteCond %{REQUEST_URI} $regex";
$rule[] = sprintf("RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]", implode('|', $methods));
$rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow));
}
// redirect with trailing slash appended
if ($hasTrailingSlash) {
$rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$';
$rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]';
}
// the main rule
$rule[] = "RewriteCond %{REQUEST_URI} $regex";
$rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]";
$rules[] = implode("\n", $rule);
$rules[] = $this->dumpRoute($name, $route, $options);
$methodVars = array_merge($methodVars, $this->getRouteMethods($route));
}
if (0 < count($methodVars)) {
$rule = array('# 405 Method Not Allowed');
$methodVars = array_values(array_unique($methodVars));
foreach ($methodVars as $i => $methodVar) {
$rule[] = sprintf('RewriteCond %%{_ROUTING__allow_%s} !-z%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : '');
$rule[] = sprintf('RewriteCond %%{_ROUTING_allow_%s} !-z%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : '');
}
$rule[] = sprintf('RewriteRule .* %s [QSA,L]', $options['script_name']);
@ -130,6 +66,100 @@ class ApacheMatcherDumper extends MatcherDumper
return implode("\n\n", $rules)."\n";
}
private function dumpRoute($name, $route, array $options)
{
$compiledRoute = $route->compile();
// prepare the apache regex
$regex = $this->regexToApacheRegex($compiledRoute->getRegex());
$regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\');
$methods = $this->getRouteMethods($route);
$hasTrailingSlash = (!$methods || in_array('HEAD', $methods)) && '/$' === substr($regex, -2) && '^/$' !== $regex;
$variables = array('E=_ROUTING_route:'.$name);
foreach ($compiledRoute->getVariables() as $i => $variable) {
$variables[] = 'E=_ROUTING_param_'.$variable.':%'.($i + 1);
}
foreach ($route->getDefaults() as $key => $value) {
$variables[] = 'E=_ROUTING_default_'.$key.':'.strtr($value, array(
':' => '\\:',
'=' => '\\=',
'\\' => '\\\\',
' ' => '\\ ',
));
}
$variables = implode(',', $variables);
$rule = array("# $name");
// method mismatch
if (0 < count($methods)) {
$allow = array();
foreach ($methods as $method) {
$methodVars[] = $method;
$allow[] = 'E=_ROUTING_allow_'.$method.':1';
}
$rule[] = "RewriteCond %{REQUEST_URI} $regex";
$rule[] = sprintf("RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]", implode('|', $methods));
$rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow));
}
// redirect with trailing slash appended
if ($hasTrailingSlash) {
$rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$';
$rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]';
}
// the main rule
$rule[] = "RewriteCond %{REQUEST_URI} $regex";
$rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]";
return implode("\n", $rule);
}
/**
* Returns methods allowed for a route
*
* @param Route $route The route
*
* @return array The methods
*/
private function getRouteMethods(Route $route)
{
$methods = array();
if ($req = $route->getRequirement('_method')) {
$methods = explode('|', strtoupper($req));
// GET and HEAD are equivalent
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
$methods[] = 'HEAD';
}
}
return $methods;
}
/**
* Converts a regex to make it suitable for mod_rewrite
*
* @param string $regex The regex
*
* @return string The converted regex
*/
private function regexToApacheRegex($regex)
{
$delimiter = $regex[0];
$regexPatternEnd = strrpos($regex, $delimiter);
if (strlen($regex) < 2 || 0 === $regexPatternEnd) {
throw new \LogicException('The "%s" route regex "%s" is invalid', $name, $regex);
}
$regex = preg_replace('/\?<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1));
return $regex;
}
/**
* Escapes a string.
*

View File

@ -4,72 +4,72 @@ RewriteRule .* - [QSA,L]
# foo
RewriteCond %{REQUEST_URI} ^/foo/(baz|symfony)$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:foo,E=_ROUTING_bar:%1,E=_ROUTING_DEFAULTS_def:test]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:foo,E=_ROUTING_param_bar:%1,E=_ROUTING_default_def:test]
# foobar
RewriteCond %{REQUEST_URI} ^/foo(?:/([^/]++))?$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:foobar,E=_ROUTING_bar:%1,E=_ROUTING_DEFAULTS_bar:toto]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:foobar,E=_ROUTING_param_bar:%1,E=_ROUTING_default_bar:toto]
# bar
RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING__allow_GET:1,E=_ROUTING__allow_HEAD:1]
RewriteRule .* - [S=1,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:bar,E=_ROUTING_foo:%1]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:bar,E=_ROUTING_param_foo:%1]
# baragain
RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING__allow_GET:1,E=_ROUTING__allow_POST:1,E=_ROUTING__allow_HEAD:1]
RewriteRule .* - [S=1,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_POST:1,E=_ROUTING_allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baragain,E=_ROUTING_foo:%1]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baragain,E=_ROUTING_param_foo:%1]
# baz
RewriteCond %{REQUEST_URI} ^/test/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz]
# baz2
RewriteCond %{REQUEST_URI} ^/test/baz\.html$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz2]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz2]
# baz3
RewriteCond %{REQUEST_URI} ^/test/baz3$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/baz3/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz3]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz3]
# baz4
RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz4,E=_ROUTING_foo:%1]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz4,E=_ROUTING_param_foo:%1]
# baz5
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
RewriteRule .* - [S=2,E=_ROUTING__allow_GET:1,E=_ROUTING__allow_HEAD:1]
RewriteRule .* - [S=2,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_HEAD:1]
RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
RewriteRule .* $0/ [QSA,L,R=301]
RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz5,E=_ROUTING_foo:%1]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz5,E=_ROUTING_param_foo:%1]
# baz5unsafe
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
RewriteCond %{REQUEST_METHOD} !^(POST)$ [NC]
RewriteRule .* - [S=1,E=_ROUTING__allow_POST:1]
RewriteRule .* - [S=1,E=_ROUTING_allow_POST:1]
RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz5unsafe,E=_ROUTING_foo:%1]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz5unsafe,E=_ROUTING_param_foo:%1]
# baz6
RewriteCond %{REQUEST_URI} ^/test/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz6,E=_ROUTING_DEFAULTS_foo:bar\ baz]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz6,E=_ROUTING_default_foo:bar\ baz]
# baz7
RewriteCond %{REQUEST_URI} ^/te\ st/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz7]
RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz7]
# 405 Method Not Allowed
RewriteCond %{_ROUTING__allow_GET} !-z [OR]
RewriteCond %{_ROUTING__allow_HEAD} !-z [OR]
RewriteCond %{_ROUTING__allow_POST} !-z
RewriteCond %{_ROUTING_allow_GET} !-z [OR]
RewriteCond %{_ROUTING_allow_HEAD} !-z [OR]
RewriteCond %{_ROUTING_allow_POST} !-z
RewriteRule .* app.php [QSA,L]

View File

@ -4,4 +4,4 @@ RewriteRule .* - [QSA,L]
# foo
RewriteCond %{REQUEST_URI} ^/foo$
RewriteRule .* ap\ p_d\ ev.php [QSA,L,E=_ROUTING__route:foo]
RewriteRule .* ap\ p_d\ ev.php [QSA,L,E=_ROUTING_route:foo]

View File

@ -51,57 +51,71 @@ class ApacheUrlMatcherTest extends \PHPUnit_Framework_TestCase
'Simple route',
'/hello/world',
array(
'_ROUTING__route' => 'hello',
'_ROUTING__controller' => 'AcmeBundle:Default:index',
'_ROUTING_name' => 'world',
'_ROUTING_route' => 'hello',
'_ROUTING_param__controller' => 'AcmeBundle:Default:index',
'_ROUTING_param_name' => 'world',
),
array(
'_route' => 'hello',
'_controller' => 'AcmeBundle:Default:index',
'name' => 'world',
'_route' => 'hello',
),
),
array(
'Route with params and defaults',
'/hello/hugo',
array(
'_ROUTING__route' => 'hello',
'_ROUTING__controller' => 'AcmeBundle:Default:index',
'_ROUTING_name' => 'hugo',
'_ROUTING_DEFAULTS_name' => 'world',
'_ROUTING_route' => 'hello',
'_ROUTING_param__controller' => 'AcmeBundle:Default:index',
'_ROUTING_param_name' => 'hugo',
'_ROUTING_default_name' => 'world',
),
array(
'name' => 'hugo',
'_route' => 'hello',
'_controller' => 'AcmeBundle:Default:index',
'_route' => 'hello',
),
),
array(
'Route with defaults only',
'/hello',
array(
'_ROUTING__route' => 'hello',
'_ROUTING__controller' => 'AcmeBundle:Default:index',
'_ROUTING_DEFAULTS_name' => 'world',
'_ROUTING_route' => 'hello',
'_ROUTING_param__controller' => 'AcmeBundle:Default:index',
'_ROUTING_default_name' => 'world',
),
array(
'name' => 'world',
'_route' => 'hello',
'_controller' => 'AcmeBundle:Default:index',
'_route' => 'hello',
),
),
array(
'REDIRECT_ envs',
'/hello/world',
array(
'REDIRECT__ROUTING__route' => 'hello',
'REDIRECT__ROUTING__controller' => 'AcmeBundle:Default:index',
'REDIRECT__ROUTING_name' => 'world',
'REDIRECT__ROUTING_route' => 'hello',
'REDIRECT__ROUTING_param__controller' => 'AcmeBundle:Default:index',
'REDIRECT__ROUTING_param_name' => 'world',
),
array(
'_route' => 'hello',
'_controller' => 'AcmeBundle:Default:index',
'name' => 'world',
'_route' => 'hello',
),
),
array(
'REDIRECT_REDIRECT_ envs',
'/hello/world',
array(
'REDIRECT_REDIRECT__ROUTING_route' => 'hello',
'REDIRECT_REDIRECT__ROUTING_param__controller' => 'AcmeBundle:Default:index',
'REDIRECT_REDIRECT__ROUTING_param_name' => 'world',
),
array(
'_controller' => 'AcmeBundle:Default:index',
'name' => 'world',
'_route' => 'hello',
),
),
);