diff --git a/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php index 3003dfdebb02..e513f04e3b44 100644 --- a/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php @@ -17,6 +17,7 @@ * ApacheUrlMatcher matches URL based on Apache mod_rewrite matching (see ApacheMatcherDumper). * * @author Fabien Potencier + * @author Arnaud Le Blanc */ class ApacheUrlMatcher extends UrlMatcher { @@ -36,36 +37,52 @@ public function match($pathinfo) $parameters = array(); $defaults = array(); $allow = array(); - $match = false; + $route = null; foreach ($_SERVER as $key => $value) { $name = $key; - if (0 === strpos($name, 'REDIRECT_')) { - $name = substr($name, 9); + // 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; } - if (0 === strpos($name, '_ROUTING_DEFAULTS_')) { - $name = substr($name, 18); - $defaults[$name] = $value; - } elseif (0 === strpos($name, '_ROUTING_')) { + while (0 === strpos($name, 'REDIRECT_')) { $name = substr($name, 9); - if ('_route' == $name) { - $match = true; - $parameters[$name] = $value; - } elseif (0 === strpos($name, '_allow_')) { - $allow[] = substr($name, 7); - } else { + } + + // expect _ROUTING__ + // or _ROUTING_ + + 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); diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php index 102488cd5c46..e9fc82d45895 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Matcher\Dumper; +use Symfony\Component\Routing\Route; + /** * Dumps a set of Apache mod_rewrite rules. * @@ -46,88 +48,116 @@ public function dump(array $options = array()) $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'; - } + $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('RewriteRule .* %s [QSA,L]', $options['script_name']); - $hasTrailingSlash = (!$methods || in_array('HEAD', $methods)) && '/$' === substr($regex, -2) && '^/$' !== $regex; + $rules[] = implode("\n", $rule); + } - $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)); - } + 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"); - // redirect with trailing slash appended - if ($hasTrailingSlash) { - $rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$'; - $rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]'; + // method mismatch + if (0 < count($methods)) { + $allow = array(); + foreach ($methods as $method) { + $methodVars[] = $method; + $allow[] = 'E=_ROUTING_allow_'.$method.':1'; } - // the main rule $rule[] = "RewriteCond %{REQUEST_URI} $regex"; - $rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]"; + $rule[] = sprintf("RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]", implode('|', $methods)); + $rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow)); + } - $rules[] = implode("\n", $rule); + // redirect with trailing slash appended + if ($hasTrailingSlash) { + $rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$'; + $rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]'; } - 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]' : ''); + // 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'; } - $rule[] = sprintf('RewriteRule .* %s [QSA,L]', $options['script_name']); + } - $rules[] = implode("\n", $rule); + 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 implode("\n\n", $rules)."\n"; + return $regex; } /** diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.apache b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.apache index 7efda77b8e7e..baa07b6d60d8 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.apache +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.apache @@ -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] diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.apache b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.apache index d16ab974bf35..309f2ff0e539 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.apache +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.apache @@ -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] diff --git a/src/Symfony/Component/Routing/Tests/Matcher/ApacheUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/ApacheUrlMatcherTest.php index 88487201cc31..3161f3ce693f 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/ApacheUrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/ApacheUrlMatcherTest.php @@ -51,57 +51,71 @@ public function getMatchData() '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( + '_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', ), ), );