bug #13567 [Routing] make host matching case-insensitive (Tobion)

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
This commit is contained in:
Fabien Potencier 2015-02-05 07:17:31 +01:00
commit 2e7434102a
8 changed files with 48 additions and 20 deletions

View File

@ -213,7 +213,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
$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) {

View File

@ -46,7 +46,7 @@ class RouteCompiler implements RouteCompilerInterface
$result = self::compilePattern($route, $host, true);
$hostVariables = $result['variables'];
$variables = array_merge($variables, $hostVariables);
$variables = $hostVariables;
$hostTokens = $result['tokens'];
$hostRegex = $result['regex'];
@ -163,7 +163,7 @@ class RouteCompiler implements RouteCompilerInterface
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,
);

View File

@ -195,7 +195,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
$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');
@ -208,7 +208,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
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');
@ -216,7 +216,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
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');
@ -224,7 +224,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
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');
@ -237,7 +237,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
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') {
@ -263,7 +263,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
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 ());

View File

@ -207,7 +207,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$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');
@ -220,7 +220,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
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');
@ -228,7 +228,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
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');
@ -236,7 +236,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
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');
@ -249,7 +249,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
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') {
@ -275,7 +275,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
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 ());

View File

@ -443,6 +443,13 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
$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'));

View File

@ -382,4 +382,25 @@ class UrlMatcherTest extends \PHPUnit_Framework_TestCase
$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('/'));
}
}

View File

@ -208,7 +208,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
'/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'),
),
),
@ -219,7 +219,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
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'),
),
@ -230,7 +230,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
'/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'),
@ -242,7 +242,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
'/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'),

View File

@ -247,7 +247,7 @@ class RouteTest extends \PHPUnit_Framework_TestCase
*/
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+'));