Skip to content

Commit

Permalink
bug #13567 [Routing] make host matching case-insensitive (Tobion)
Browse files Browse the repository at this point in the history
This PR was merged into the 2.3 branch.

Discussion
----------

[Routing] make host matching case-insensitive

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #9072
| License       | MIT
| Doc PR        |

Ignore case in host which means:
- When generating URLs we leave the case in the host as specified.
- When matching we always return lower-cased versions of parameters (because of https://github.com/symfony/symfony/blob/2.7/src/Symfony/Component/Routing/RequestContext.php#L190 ) in the host. This is also what browers do. They lowercase the host before sending the request, i.e. WWW.eXample.org is sent as www.example.org. But when using curl for example it sends the host as-is. So the HttpFoundation Request class can actually have a non-lowercased host because it doesn't have this normalization.

Commits
-------

952388c [Routing] make host matching case-insensitive according to RFC 3986
  • Loading branch information
fabpot committed Feb 5, 2015
2 parents 051402e + 952388c commit 2e74341
Show file tree
Hide file tree
Showing 8 changed files with 48 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/Symfony/Component/Routing/Generator/UrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
$routeHost = '';
foreach ($hostTokens as $token) {
if ('variable' === $token[0]) {
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) {
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#i', $mergedParams[$token[3]])) {
$message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]);

if ($this->strictRequirements) {
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/Routing/RouteCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static function compile(Route $route)
$result = self::compilePattern($route, $host, true);

$hostVariables = $result['variables'];
$variables = array_merge($variables, $hostVariables);
$variables = $hostVariables;

$hostTokens = $result['tokens'];
$hostRegex = $result['regex'];
Expand Down Expand Up @@ -163,7 +163,7 @@ private static function compilePattern(Route $route, $pattern, $isHost)

return array(
'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
'regex' => self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s',
'regex' => self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s'.($isHost ? 'i' : ''),
'tokens' => array_reverse($tokens),
'variables' => $variables,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ public function match($pathinfo)

$host = $this->context->getHost();

if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route1
if ($pathinfo === '/route1') {
return array('_route' => 'route1');
Expand All @@ -208,23 +208,23 @@ public function match($pathinfo)

}

if (preg_match('#^b\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^b\\.example\\.com$#si', $host, $hostMatches)) {
// route3
if ($pathinfo === '/c2/route3') {
return array('_route' => 'route3');
}

}

if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route4
if ($pathinfo === '/route4') {
return array('_route' => 'route4');
}

}

if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route5
if ($pathinfo === '/route5') {
return array('_route' => 'route5');
Expand All @@ -237,7 +237,7 @@ public function match($pathinfo)
return array('_route' => 'route6');
}

if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#si', $host, $hostMatches)) {
if (0 === strpos($pathinfo, '/route1')) {
// route11
if ($pathinfo === '/route11') {
Expand All @@ -263,7 +263,7 @@ public function match($pathinfo)

}

if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route15
if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public function match($pathinfo)

$host = $this->context->getHost();

if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route1
if ($pathinfo === '/route1') {
return array('_route' => 'route1');
Expand All @@ -220,23 +220,23 @@ public function match($pathinfo)

}

if (preg_match('#^b\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^b\\.example\\.com$#si', $host, $hostMatches)) {
// route3
if ($pathinfo === '/c2/route3') {
return array('_route' => 'route3');
}

}

if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^a\\.example\\.com$#si', $host, $hostMatches)) {
// route4
if ($pathinfo === '/route4') {
return array('_route' => 'route4');
}

}

if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route5
if ($pathinfo === '/route5') {
return array('_route' => 'route5');
Expand All @@ -249,7 +249,7 @@ public function match($pathinfo)
return array('_route' => 'route6');
}

if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#si', $host, $hostMatches)) {
if (0 === strpos($pathinfo, '/route1')) {
// route11
if ($pathinfo === '/route11') {
Expand All @@ -275,7 +275,7 @@ public function match($pathinfo)

}

if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
if (preg_match('#^c\\.example\\.com$#si', $host, $hostMatches)) {
// route15
if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,13 @@ public function testUrlWithInvalidParameterInHostInNonStrictMode()
$this->assertNull($generator->generate('test', array('foo' => 'baz'), false));
}

public function testHostIsCaseInsensitive()
{
$routes = $this->getRoutes('test', new Route('/', array(), array('locale' => 'en|de|fr'), array(), '{locale}.FooBar.com'));
$generator = $this->getGenerator($routes);
$this->assertSame('//EN.FooBar.com/app.php/', $generator->generate('test', array('locale' => 'EN'), UrlGeneratorInterface::NETWORK_PATH));
}

public function testGenerateNetworkPath()
{
$routes = $this->getRoutes('test', new Route('/{name}', array(), array('_scheme' => 'http'), array(), '{locale}.example.com'));
Expand Down
21 changes: 21 additions & 0 deletions src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,25 @@ public function testWithOutHostHostDoesNotMatch()
$matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'example.com'));
$matcher->match('/foo/bar');
}

/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
public function testPathIsCaseSensitive()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/locale', array(), array('locale' => 'EN|FR|DE')));

$matcher = new UrlMatcher($coll, new RequestContext());
$matcher->match('/en');
}

public function testHostIsCaseInsensitive()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/', array(), array('locale' => 'EN|FR|DE'), array(), '{locale}.example.com'));

$matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com'));
$this->assertEquals(array('_route' => 'foo', 'locale' => 'en'), $matcher->match('/'));
}
}
8 changes: 4 additions & 4 deletions src/Symfony/Component/Routing/Tests/RouteCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public function provideCompileWithHostData()
'/hello', '#^/hello$#s', array(), array(), array(
array('text', '/hello'),
),
'#^www\.example\.com$#s', array(), array(
'#^www\.example\.com$#si', array(), array(
array('text', 'www.example.com'),
),
),
Expand All @@ -219,7 +219,7 @@ public function provideCompileWithHostData()
array('variable', '/', '[^/]++', 'name'),
array('text', '/hello'),
),
'#^www\.example\.(?P<tld>[^\.]++)$#s', array('tld'), array(
'#^www\.example\.(?P<tld>[^\.]++)$#si', array('tld'), array(
array('variable', '.', '[^\.]++', 'tld'),
array('text', 'www.example'),
),
Expand All @@ -230,7 +230,7 @@ public function provideCompileWithHostData()
'/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
array('text', '/hello'),
),
'#^(?P<locale>[^\.]++)\.example\.(?P<tld>[^\.]++)$#s', array('locale', 'tld'), array(
'#^(?P<locale>[^\.]++)\.example\.(?P<tld>[^\.]++)$#si', array('locale', 'tld'), array(
array('variable', '.', '[^\.]++', 'tld'),
array('text', '.example'),
array('variable', '', '[^\.]++', 'locale'),
Expand All @@ -242,7 +242,7 @@ public function provideCompileWithHostData()
'/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
array('text', '/hello'),
),
'#^(?P<locale>[^\.]++)\.example\.(?P<tld>[^\.]++)$#s', array('locale', 'tld'), array(
'#^(?P<locale>[^\.]++)\.example\.(?P<tld>[^\.]++)$#si', array('locale', 'tld'), array(
array('variable', '.', '[^\.]++', 'tld'),
array('text', '.example'),
array('variable', '', '[^\.]++', 'locale'),
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Routing/Tests/RouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ public function testSerializeWhenCompiled()
*/
public function testSerializedRepresentationKeepsWorking()
{
$serialized = 'C:31:"Symfony\Component\Routing\Route":933:{a:8:{s:4:"path";s:13:"/prefix/{foo}";s:4:"host";s:20:"{locale}.example.net";s:8:"defaults";a:1:{s:3:"foo";s:7:"default";}s:12:"requirements";a:1:{s:3:"foo";s:3:"\d+";}s:7:"options";a:1:{s:14:"compiler_class";s:39:"Symfony\Component\Routing\RouteCompiler";}s:7:"schemes";a:0:{}s:7:"methods";a:0:{}s:8:"compiled";C:39:"Symfony\Component\Routing\CompiledRoute":568:{a:8:{s:4:"vars";a:2:{i:0;s:6:"locale";i:1;s:3:"foo";}s:11:"path_prefix";s:7:"/prefix";s:10:"path_regex";s:30:"#^/prefix(?:/(?P<foo>\d+))?$#s";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:3:"foo";}i:1;a:2:{i:0;s:4:"text";i:1;s:7:"/prefix";}}s:9:"path_vars";a:1:{i:0;s:3:"foo";}s:10:"host_regex";s:38:"#^(?P<locale>[^\.]++)\.example\.net$#s";s:11:"host_tokens";a:2:{i:0;a:2:{i:0;s:4:"text";i:1;s:12:".example.net";}i:1;a:4:{i:0;s:8:"variable";i:1;s:0:"";i:2;s:7:"[^\.]++";i:3;s:6:"locale";}}s:9:"host_vars";a:1:{i:0;s:6:"locale";}}}}}';
$serialized = 'C:31:"Symfony\Component\Routing\Route":934:{a:8:{s:4:"path";s:13:"/prefix/{foo}";s:4:"host";s:20:"{locale}.example.net";s:8:"defaults";a:1:{s:3:"foo";s:7:"default";}s:12:"requirements";a:1:{s:3:"foo";s:3:"\d+";}s:7:"options";a:1:{s:14:"compiler_class";s:39:"Symfony\Component\Routing\RouteCompiler";}s:7:"schemes";a:0:{}s:7:"methods";a:0:{}s:8:"compiled";C:39:"Symfony\Component\Routing\CompiledRoute":569:{a:8:{s:4:"vars";a:2:{i:0;s:6:"locale";i:1;s:3:"foo";}s:11:"path_prefix";s:7:"/prefix";s:10:"path_regex";s:30:"#^/prefix(?:/(?P<foo>\d+))?$#s";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:3:"foo";}i:1;a:2:{i:0;s:4:"text";i:1;s:7:"/prefix";}}s:9:"path_vars";a:1:{i:0;s:3:"foo";}s:10:"host_regex";s:39:"#^(?P<locale>[^\.]++)\.example\.net$#si";s:11:"host_tokens";a:2:{i:0;a:2:{i:0;s:4:"text";i:1;s:12:".example.net";}i:1;a:4:{i:0;s:8:"variable";i:1;s:0:"";i:2;s:7:"[^\.]++";i:3;s:6:"locale";}}s:9:"host_vars";a:1:{i:0;s:6:"locale";}}}}}';
$unserialized = unserialize($serialized);

$route = new Route('/prefix/{foo}', array('foo' => 'default'), array('foo' => '\d+'));
Expand Down

0 comments on commit 2e74341

Please sign in to comment.