feature #26304 [Routing] support scheme requirement without redirectable dumped matcher (Tobion)

This PR was merged into the 4.1-dev branch.

Discussion
----------

[Routing] support scheme requirement without redirectable dumped matcher

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets |
| License       | MIT
| Doc PR        |

The scheme handling was just redirecting immediately without testing the other routes at all for potential matches. This can cause Problems when you try to have different routes/controllers for the same path but different schemes. See added test.

```
        $coll = new RouteCollection();
        $coll->add('https_route', new Route('/', array(), array(), array(), '', array('https')));
        $coll->add('http_route', new Route('/', array(), array(), array(), '', array('http')));
        $matcher = $this->getUrlMatcher($coll);
        $this->assertEquals(array('_route' => 'http_route'), $matcher->match('/'));
```

Instead of matching the right route, it would redirect immediatly as soon as it hits the first route.
This does not make sense and is not consistent with the other logic. Redirection should only happen when nothing matches. While fixing this I could also remove the limitation

> throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');

If redirection is not possible, the wrong scheme will just not match the route. This is the same as already implemented by UrlMatcher (not RedirecableUrlMatcher). If redirection is supported, it will redirect to the first supported scheme if no other route matches. This makes the implementation similar to redirection for trailing slash and handling not allowed methods.

Also previously, the scheme redirection was done for non-safe verbs which shouldn't happen as well, ref. #25962

Commits
-------

f9b54c5 [Routing] support scheme requirement without redirectable dumped matcher
This commit is contained in:
Nicolas Grekas 2018-02-28 12:38:29 +01:00
commit a1b241473d
21 changed files with 381 additions and 202 deletions

View File

@ -29,7 +29,6 @@ class PhpMatcherDumper extends MatcherDumper
{
private $expressionLanguage;
private $signalingException;
private $supportsRedirections;
/**
* @var ExpressionFunctionProviderInterface[]
@ -57,7 +56,7 @@ class PhpMatcherDumper extends MatcherDumper
// trailing slash support is only enabled if we know how to redirect the user
$interfaces = class_implements($options['base_class']);
$this->supportsRedirections = isset($interfaces[RedirectableUrlMatcherInterface::class]);
$supportsRedirections = isset($interfaces[RedirectableUrlMatcherInterface::class]);
return <<<EOF
<?php
@ -77,7 +76,7 @@ class {$options['class']} extends {$options['base_class']}
\$this->context = \$context;
}
{$this->generateMatchMethod()}
{$this->generateMatchMethod($supportsRedirections)}
}
EOF;
@ -91,7 +90,7 @@ EOF;
/**
* Generates the code for the match method implementing UrlMatcherInterface.
*/
private function generateMatchMethod(): string
private function generateMatchMethod(bool $supportsRedirections): string
{
// Group hosts by same-suffix, re-order when possible
$matchHost = false;
@ -111,7 +110,7 @@ EOF;
$code = <<<EOF
{
\$allow = array();
\$allow = \$allowSchemes = array();
\$pathinfo = rawurldecode(\$rawPathinfo);
\$context = \$this->context;
\$requestMethod = \$canonicalMethod = \$context->getMethod();
@ -124,25 +123,44 @@ $code
EOF;
if ($this->supportsRedirections) {
if ($supportsRedirections) {
return <<<'EOF'
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
$allow = $allowSchemes = array();
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
// no-op
} elseif ($allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(key($allowSchemes));
try {
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
}
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' !== $pathinfo) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
if ($allowSchemes) {
goto redirect_scheme;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
throw new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array
EOF
.$code."\n return null;\n }";
@ -238,9 +256,6 @@ EOF
}
if (!$route->getCondition()) {
if (!$this->supportsRedirections && $route->getSchemes()) {
throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
}
$default .= sprintf(
"%s => array(%s, %s, %s, %s),\n",
self::export($url),
@ -535,8 +550,8 @@ EOF;
} else {
$code = '';
}
if ($this->supportsRedirections) {
$code .= <<<EOF
$code .= <<<EOF
\$hasRequiredScheme = !\$requiredSchemes || isset(\$requiredSchemes[\$context->getScheme()]);
if (\$requiredMethods && !isset(\$requiredMethods[\$canonicalMethod]) && !isset(\$requiredMethods[\$requestMethod])) {
@ -546,28 +561,13 @@ EOF;
break;
}
if (!\$hasRequiredScheme) {
if ('GET' !== \$canonicalMethod) {
break;
}
return \$this->redirect(\$rawPathinfo, \$ret['_route'], key(\$requiredSchemes)) + \$ret;
}
return \$ret;
EOF;
} else {
$code .= <<<EOF
if (\$requiredMethods && !isset(\$requiredMethods[\$canonicalMethod]) && !isset(\$requiredMethods[\$requestMethod])) {
\$allow += \$requiredMethods;
\$allowSchemes += \$requiredSchemes;
break;
}
return \$ret;
EOF;
}
return $code;
}
@ -647,9 +647,6 @@ EOF;
}
if ($schemes = $route->getSchemes()) {
if (!$this->supportsRedirections) {
throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
}
$schemes = self::export(array_flip($schemes));
if ($methods) {
$code .= <<<EOF
@ -662,11 +659,8 @@ EOF;
goto $gotoname;
}
if (!\$hasRequiredScheme) {
if ('GET' !== \$canonicalMethod) {
goto $gotoname;
}
return \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)) + \$ret;
\$allowSchemes += \$requiredSchemes;
goto $gotoname;
}
@ -675,11 +669,8 @@ EOF;
$code .= <<<EOF
\$requiredSchemes = $schemes;
if (!isset(\$requiredSchemes[\$context->getScheme()])) {
if ('GET' !== \$canonicalMethod) {
goto $gotoname;
}
return \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)) + \$ret;
\$allowSchemes += \$requiredSchemes;
goto $gotoname;
}

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\Routing\Exception\ExceptionInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Route;
/**
* @author Fabien Potencier <fabien@symfony.com>
@ -27,38 +27,38 @@ abstract class RedirectableUrlMatcher extends UrlMatcher implements Redirectable
try {
return parent::match($pathinfo);
} catch (ResourceNotFoundException $e) {
if ('/' === $pathinfo || !\in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
if (!\in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
throw $e;
}
try {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
$ret = parent::match($pathinfo);
if ($this->allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(current($this->allowSchemes));
try {
$ret = parent::match($pathinfo);
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
} catch (ResourceNotFoundException $e2) {
return $this->redirect($pathinfo, $ret['_route'] ?? null, $this->context->getScheme()) + $ret;
} catch (ExceptionInterface $e2) {
throw $e;
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' === $pathinfo) {
throw $e;
} else {
try {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
$ret = parent::match($pathinfo);
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
} catch (ExceptionInterface $e2) {
if ($this->allowSchemes) {
goto redirect_scheme;
}
throw $e;
}
}
}
}
/**
* {@inheritdoc}
*/
protected function handleRouteRequirements($pathinfo, $name, Route $route)
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) {
return array(self::REQUIREMENT_MISMATCH, null);
}
// check HTTP scheme requirement
$scheme = $this->context->getScheme();
$schemes = $route->getSchemes();
if ($schemes && !$route->hasScheme($scheme)) {
return array(self::ROUTE_MATCH, $this->redirect($pathinfo, $name, current($schemes)));
}
return array(self::REQUIREMENT_MATCH, null);
}
}

View File

@ -33,7 +33,19 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
const ROUTE_MATCH = 2;
protected $context;
/**
* Collects HTTP methods that would be allowed for the request.
*/
protected $allow = array();
/**
* Collects URI schemes that would be allowed for the request.
*
* @internal
*/
protected $allowSchemes = array();
protected $routes;
protected $request;
protected $expressionLanguage;
@ -70,7 +82,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
*/
public function match($pathinfo)
{
$this->allow = array();
$this->allow = $this->allowSchemes = array();
if ($ret = $this->matchCollection(rawurldecode($pathinfo), $this->routes)) {
return $ret;
@ -141,7 +153,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
continue;
}
// check HTTP method requirement
$hasRequiredScheme = !$route->getSchemes() || $route->hasScheme($this->context->getScheme());
if ($requiredMethods = $route->getMethods()) {
// HEAD and GET are equivalent as per RFC
if ('HEAD' === $method = $this->context->getMethod()) {
@ -149,7 +161,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
}
if (!in_array($method, $requiredMethods)) {
if (self::REQUIREMENT_MATCH === $status[0]) {
if ($hasRequiredScheme) {
$this->allow = array_merge($this->allow, $requiredMethods);
}
@ -157,6 +169,12 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
}
}
if (!$hasRequiredScheme) {
$this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
continue;
}
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : array()));
}
}
@ -197,11 +215,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
return array(self::REQUIREMENT_MISMATCH, null);
}
// check HTTP scheme requirement
$scheme = $this->context->getScheme();
$status = $route->getSchemes() && !$route->hasScheme($scheme) ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH;
return array($status, null);
return array(self::REQUIREMENT_MATCH, null);
}
/**

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -64,8 +64,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}
@ -209,8 +216,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -2799,8 +2799,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}

View File

@ -17,23 +17,42 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
$allow = $allowSchemes = array();
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
// no-op
} elseif ($allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(key($allowSchemes));
try {
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
}
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' !== $pathinfo) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
if ($allowSchemes) {
goto redirect_scheme;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
throw new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -113,11 +132,8 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
if (!$hasRequiredScheme) {
if ('GET' !== $canonicalMethod) {
break;
}
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
$allowSchemes += $requiredSchemes;
break;
}
return $ret;

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -69,8 +69,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -17,23 +17,42 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
$allow = $allowSchemes = array();
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
// no-op
} elseif ($allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(key($allowSchemes));
try {
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
}
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' !== $pathinfo) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
if ($allowSchemes) {
goto redirect_scheme;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
throw new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -90,11 +109,8 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
if (!$hasRequiredScheme) {
if ('GET' !== $canonicalMethod) {
break;
}
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
$allowSchemes += $requiredSchemes;
break;
}
return $ret;
@ -245,11 +261,8 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
if (!$hasRequiredScheme) {
if ('GET' !== $canonicalMethod) {
break;
}
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
$allowSchemes += $requiredSchemes;
break;
}
return $ret;

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -43,8 +43,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo];
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}
@ -74,8 +81,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -60,8 +60,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo];
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}

View File

@ -17,23 +17,42 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
$allow = $allowSchemes = array();
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
// no-op
} elseif ($allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(key($allowSchemes));
try {
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
}
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' !== $pathinfo) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
if ($allowSchemes) {
goto redirect_scheme;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
throw new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -72,11 +91,8 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
if (!$hasRequiredScheme) {
if ('GET' !== $canonicalMethod) {
break;
}
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
$allowSchemes += $requiredSchemes;
break;
}
return $ret;
@ -115,11 +131,8 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
if (!$hasRequiredScheme) {
if ('GET' !== $canonicalMethod) {
break;
}
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
$allowSchemes += $requiredSchemes;
break;
}
return $ret;

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -44,8 +44,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo];
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}
@ -93,8 +100,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}

View File

@ -17,23 +17,42 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
$allow = $allowSchemes = array();
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
if ($allow) {
throw new MethodNotAllowedException(array_keys($allow));
}
if (!in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
// no-op
} elseif ($allowSchemes) {
redirect_scheme:
$scheme = $this->context->getScheme();
$this->context->setScheme(key($allowSchemes));
try {
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
}
} finally {
$this->context->setScheme($scheme);
}
} elseif ('/' !== $pathinfo) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
if ($allowSchemes) {
goto redirect_scheme;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
throw new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -68,11 +87,8 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
if (!$hasRequiredScheme) {
if ('GET' !== $canonicalMethod) {
break;
}
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
$allowSchemes += $requiredSchemes;
break;
}
return $ret;
@ -127,11 +143,8 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
if (!$hasRequiredScheme) {
if ('GET' !== $canonicalMethod) {
break;
}
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
$allowSchemes += $requiredSchemes;
break;
}
return $ret;

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -57,8 +57,15 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
}
}
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
$allow += $requiredMethods;
if ($hasRequiredScheme) {
$allow += $requiredMethods;
}
break;
}
if (!$hasRequiredScheme) {
$allowSchemes += $requiredSchemes;
break;
}

View File

@ -17,7 +17,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
public function match($rawPathinfo)
{
$allow = array();
$allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo);
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -17,24 +17,6 @@ use Symfony\Component\Routing\RequestContext;
class DumpedUrlMatcherTest extends UrlMatcherTest
{
/**
* @expectedException \LogicException
* @expectedExceptionMessage The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.
*/
public function testSchemeRequirement()
{
parent::testSchemeRequirement();
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.
*/
public function testSchemeAndMethodMismatch()
{
parent::testSchemeRequirement();
}
protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null)
{
static $i = 0;

View File

@ -46,24 +46,6 @@ class PhpMatcherDumperTest extends TestCase
@unlink($this->dumpPath);
}
/**
* @expectedException \LogicException
*/
public function testDumpWhenSchemeIsUsedWithoutAProperDumper()
{
$collection = new RouteCollection();
$collection->add('secure', new Route(
'/secure',
array(),
array(),
array(),
'',
array('https')
));
$dumper = new PhpMatcherDumper($collection);
$dumper->dump();
}
public function testRedirectPreservesUrlEncoding()
{
$collection = new RouteCollection();

View File

@ -17,7 +17,7 @@ use Symfony\Component\Routing\RequestContext;
class RedirectableUrlMatcherTest extends UrlMatcherTest
{
public function testRedirectWhenNoSlash()
public function testMissingTrailingSlash()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo/'));
@ -27,7 +27,7 @@ class RedirectableUrlMatcherTest extends UrlMatcherTest
$matcher->match('/foo');
}
public function testRedirectWhenSlash()
public function testExtraTrailingSlash()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo'));
@ -127,6 +127,16 @@ class RedirectableUrlMatcherTest extends UrlMatcherTest
$this->assertSame(array('_route' => 'foo'), $matcher->match('/foo'));
}
public function testMissingTrailingSlashAndScheme()
{
$coll = new RouteCollection();
$coll->add('foo', (new Route('/foo/'))->setSchemes(array('https')));
$matcher = $this->getUrlMatcher($coll);
$matcher->expects($this->once())->method('redirect')->with('/foo/', 'foo', 'https')->will($this->returnValue(array()));
$matcher->match('/foo');
}
protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null)
{
return $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($routes, $context ?: new RequestContext()));

View File

@ -325,6 +325,58 @@ class UrlMatcherTest extends TestCase
$matcher->match('/do.t.html');
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
public function testMissingTrailingSlash()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo/'));
$matcher = $this->getUrlMatcher($coll);
$matcher->match('/foo');
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
public function testExtraTrailingSlash()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo'));
$matcher = $this->getUrlMatcher($coll);
$matcher->match('/foo/');
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
public function testMissingTrailingSlashForNonSafeMethod()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo/'));
$context = new RequestContext();
$context->setMethod('POST');
$matcher = $this->getUrlMatcher($coll, $context);
$matcher->match('/foo');
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
public function testExtraTrailingSlashForNonSafeMethod()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo'));
$context = new RequestContext();
$context->setMethod('POST');
$matcher = $this->getUrlMatcher($coll, $context);
$matcher->match('/foo/');
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
@ -336,6 +388,29 @@ class UrlMatcherTest extends TestCase
$matcher->match('/foo');
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
public function testSchemeRequirementForNonSafeMethod()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo', array(), array(), array(), '', array('https')));
$context = new RequestContext();
$context->setMethod('POST');
$matcher = $this->getUrlMatcher($coll, $context);
$matcher->match('/foo');
}
public function testSamePathWithDifferentScheme()
{
$coll = new RouteCollection();
$coll->add('https_route', new Route('/', array(), array(), array(), '', array('https')));
$coll->add('http_route', new Route('/', array(), array(), array(), '', array('http')));
$matcher = $this->getUrlMatcher($coll);
$this->assertEquals(array('_route' => 'http_route'), $matcher->match('/'));
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/