diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index 580c5a892f945..d1affcba9d995 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -29,7 +29,6 @@ class PhpMatcherDumper extends MatcherDumper { private $expressionLanguage; private $signalingException; - private $supportsRedirections; /** * @var ExpressionFunctionProviderInterface[] @@ -57,7 +56,7 @@ public function dump(array $options = array()) // 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 <<context = \$context; } -{$this->generateMatchMethod()} +{$this->generateMatchMethod($supportsRedirections)} } EOF; @@ -91,7 +90,7 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac /** * 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 @@ private function generateMatchMethod(): string $code = <<context; \$requestMethod = \$canonicalMethod = \$context->getMethod(); @@ -124,25 +123,44 @@ private function generateMatchMethod(): string 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 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri } 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 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string } else { $code = ''; } - if ($this->supportsRedirections) { - $code .= <<getScheme()]); if (\$requiredMethods && !isset(\$requiredMethods[\$canonicalMethod]) && !isset(\$requiredMethods[\$requestMethod])) { @@ -546,28 +561,13 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string break; } if (!\$hasRequiredScheme) { - if ('GET' !== \$canonicalMethod) { - break; - } - - return \$this->redirect(\$rawPathinfo, \$ret['_route'], key(\$requiredSchemes)) + \$ret; - } - - return \$ret; - -EOF; - } else { - $code .= <<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 .= <<redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)) + \$ret; + \$allowSchemes += \$requiredSchemes; + goto $gotoname; } @@ -675,11 +669,8 @@ private function compileRoute(Route $route, string $name, bool $checkHost): stri $code .= <<getScheme()])) { - if ('GET' !== \$canonicalMethod) { - goto $gotoname; - } - - return \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)) + \$ret; + \$allowSchemes += \$requiredSchemes; + goto $gotoname; } diff --git a/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php index 33f8642ba7eb4..e60552f158230 100644 --- a/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php @@ -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 @@ -27,38 +27,38 @@ public function match($pathinfo) 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); - /** - * {@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 $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret; + } catch (ExceptionInterface $e2) { + if ($this->allowSchemes) { + goto redirect_scheme; + } + throw $e; + } + } } - - return array(self::REQUIREMENT_MATCH, null); } } diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index 23cfd5f19f185..7b71526f469f6 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -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 @@ public function getContext() */ 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 @@ protected function matchCollection($pathinfo, RouteCollection $routes) 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 @@ protected function matchCollection($pathinfo, RouteCollection $routes) } if (!in_array($method, $requiredMethods)) { - if (self::REQUIREMENT_MATCH === $status[0]) { + if ($hasRequiredScheme) { $this->allow = array_merge($this->allow, $requiredMethods); } @@ -157,6 +169,12 @@ protected function matchCollection($pathinfo, RouteCollection $routes) } } + 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 @@ protected function handleRouteRequirements($pathinfo, $name, Route $route) 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); } /** diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php index c61280ac3900b..37d5d950e38f0 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php index 078cf35ff8ac2..97a544b7ba17a 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); @@ -64,8 +64,15 @@ public function match($rawPathinfo) } } + $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 @@ public function match($rawPathinfo) } } + $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; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php index 425bdf34bca0c..41669365689ca 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); @@ -2799,8 +2799,15 @@ public function match($rawPathinfo) } } + $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; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php index e6a580bcff705..2167b4e917a16 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php @@ -17,23 +17,42 @@ public function __construct(RequestContext $context) 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 @@ private function doMatch(string $rawPathinfo, array &$allow = array()): ?array break; } if (!$hasRequiredScheme) { - if ('GET' !== $canonicalMethod) { - break; - } - - return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret; + $allowSchemes += $requiredSchemes; + break; } return $ret; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php index 903b937b9e7a8..be3d1f437326f 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); @@ -69,8 +69,15 @@ public function match($rawPathinfo) } } + $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; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php index 595ced7592313..f31a0544fe818 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php index 8d2bcbf6737c5..acffa8e6a2bd5 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php @@ -17,23 +17,42 @@ public function __construct(RequestContext $context) 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 @@ private function doMatch(string $rawPathinfo, array &$allow = array()): ?array 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 @@ private function doMatch(string $rawPathinfo, array &$allow = array()): ?array break; } if (!$hasRequiredScheme) { - if ('GET' !== $canonicalMethod) { - break; - } - - return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret; + $allowSchemes += $requiredSchemes; + break; } return $ret; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php index 97cfa9bd72120..fc5c9853a22c0 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); @@ -43,8 +43,15 @@ public function match($rawPathinfo) } 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 @@ public function match($rawPathinfo) } } + $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; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php index f65be557062a3..a5ea6c6276b44 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); @@ -60,8 +60,15 @@ public function match($rawPathinfo) } 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; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php index 2b0f66e8c1bcb..152cc61cec2b3 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php @@ -17,23 +17,42 @@ public function __construct(RequestContext $context) 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 @@ private function doMatch(string $rawPathinfo, array &$allow = array()): ?array 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 @@ private function doMatch(string $rawPathinfo, array &$allow = array()): ?array break; } if (!$hasRequiredScheme) { - if ('GET' !== $canonicalMethod) { - break; - } - - return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret; + $allowSchemes += $requiredSchemes; + break; } return $ret; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php index 5b07bd949df6d..960e20157dba4 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); @@ -44,8 +44,15 @@ public function match($rawPathinfo) } 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 @@ public function match($rawPathinfo) } } + $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; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php index 59566b62d7a63..591b526c14e28 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php @@ -17,23 +17,42 @@ public function __construct(RequestContext $context) 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 @@ private function doMatch(string $rawPathinfo, array &$allow = array()): ?array 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 @@ private function doMatch(string $rawPathinfo, array &$allow = array()): ?array break; } if (!$hasRequiredScheme) { - if ('GET' !== $canonicalMethod) { - break; - } - - return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret; + $allowSchemes += $requiredSchemes; + break; } return $ret; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php index 925e340368b9c..0e1d0c8e95fc0 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); @@ -57,8 +57,15 @@ public function match($rawPathinfo) } } + $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; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php index 2e295b29023ba..c459408d70a9f 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php @@ -17,7 +17,7 @@ public function __construct(RequestContext $context) public function match($rawPathinfo) { - $allow = array(); + $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo); $context = $this->context; $requestMethod = $canonicalMethod = $context->getMethod(); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/DumpedUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/DumpedUrlMatcherTest.php index e36a0220d9c2d..20feb8efa00f6 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/DumpedUrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/DumpedUrlMatcherTest.php @@ -17,24 +17,6 @@ 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; diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php index 2ccef5359c5dc..4bbfe131a7bf9 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php @@ -46,24 +46,6 @@ protected function tearDown() @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(); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php index 1cbfcfd0262b0..4a962f916e634 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php @@ -17,7 +17,7 @@ class RedirectableUrlMatcherTest extends UrlMatcherTest { - public function testRedirectWhenNoSlash() + public function testMissingTrailingSlash() { $coll = new RouteCollection(); $coll->add('foo', new Route('/foo/')); @@ -27,7 +27,7 @@ public function testRedirectWhenNoSlash() $matcher->match('/foo'); } - public function testRedirectWhenSlash() + public function testExtraTrailingSlash() { $coll = new RouteCollection(); $coll->add('foo', new Route('/foo')); @@ -127,6 +127,16 @@ public function testSchemeRequirement() $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())); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index 29e695e7874a4..a18a84f701676 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -325,6 +325,58 @@ public function testDefaultRequirementOfVariableDisallowsNextSeparator() $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 @@ public function testSchemeRequirement() $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 */