Skip to content

Commit beec4f7

Browse files
Tobionnicolas-grekas
authored andcommitted
[Routing] support scheme requirement without redirectable dumped matcher
1 parent 308e12c commit beec4f7

22 files changed

+365
-231
lines changed

src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php

+34-45
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class PhpMatcherDumper extends MatcherDumper
2929
{
3030
private $expressionLanguage;
3131
private $signalingException;
32-
private $supportsRedirections;
3332

3433
/**
3534
* @var ExpressionFunctionProviderInterface[]
@@ -57,7 +56,7 @@ public function dump(array $options = array())
5756

5857
// trailing slash support is only enabled if we know how to redirect the user
5958
$interfaces = class_implements($options['base_class']);
60-
$this->supportsRedirections = isset($interfaces[RedirectableUrlMatcherInterface::class]);
59+
$supportsRedirections = isset($interfaces[RedirectableUrlMatcherInterface::class]);
6160

6261
return <<<EOF
6362
<?php
@@ -77,7 +76,7 @@ public function __construct(RequestContext \$context)
7776
\$this->context = \$context;
7877
}
7978
80-
{$this->generateMatchMethod()}
79+
{$this->generateMatchMethod($supportsRedirections)}
8180
}
8281
8382
EOF;
@@ -91,7 +90,7 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
9190
/**
9291
* Generates the code for the match method implementing UrlMatcherInterface.
9392
*/
94-
private function generateMatchMethod(): string
93+
private function generateMatchMethod(bool $supportsRedirections): string
9594
{
9695
// Group hosts by same-suffix, re-order when possible
9796
$matchHost = false;
@@ -111,7 +110,7 @@ private function generateMatchMethod(): string
111110

112111
$code = <<<EOF
113112
{
114-
\$allow = array();
113+
\$allow = \$allowSchemes = array();
115114
\$pathinfo = rawurldecode(\$rawPathinfo);
116115
\$context = \$this->context;
117116
\$requestMethod = \$canonicalMethod = \$context->getMethod();
@@ -124,25 +123,42 @@ private function generateMatchMethod(): string
124123
125124
EOF;
126125

127-
if ($this->supportsRedirections) {
126+
if ($supportsRedirections) {
128127
return <<<'EOF'
129128
public function match($pathinfo)
130129
{
131-
$allow = array();
132-
if ($ret = $this->doMatch($pathinfo, $allow)) {
130+
$allow = $allowSchemes = array();
131+
if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
133132
return $ret;
134133
}
135-
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
134+
if ($allow || !in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
135+
// no-op
136+
} elseif ($allowSchemes) {
137+
redirect_scheme:
138+
$scheme = $this->context->getScheme();
139+
$this->context->setScheme(key($allowSchemes));
140+
try {
141+
if ($ret = $this->doMatch($pathinfo)) {
142+
return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret;
143+
}
144+
} finally {
145+
$this->context->setScheme($scheme);
146+
}
147+
} elseif ('/' !== $pathinfo) {
148+
$allow2 = array();
136149
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
137-
if ($ret = $this->doMatch($pathinfo)) {
150+
if ($ret = $this->doMatch($pathinfo, $allow2, $allowSchemes)) {
138151
return $this->redirect($pathinfo, $ret['_route']) + $ret;
139152
}
153+
if ($allowSchemes) {
154+
goto redirect_scheme;
155+
}
140156
}
141157
142158
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
143159
}
144160
145-
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
161+
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array
146162

147163
EOF
148164
.$code."\n return null;\n }";
@@ -238,9 +254,6 @@ private function compileStaticRoutes(array $staticRoutes, bool $matchHost): stri
238254
}
239255

240256
if (!$route->getCondition()) {
241-
if (!$this->supportsRedirections && $route->getSchemes()) {
242-
throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
243-
}
244257
$default .= sprintf(
245258
"%s => array(%s, %s, %s, %s),\n",
246259
self::export($url),
@@ -535,8 +548,8 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
535548
} else {
536549
$code = '';
537550
}
538-
if ($this->supportsRedirections) {
539-
$code .= <<<EOF
551+
552+
$code .= <<<EOF
540553
541554
\$hasRequiredScheme = !\$requiredSchemes || isset(\$requiredSchemes[\$context->getScheme()]);
542555
if (\$requiredMethods && !isset(\$requiredMethods[\$canonicalMethod]) && !isset(\$requiredMethods[\$requestMethod])) {
@@ -546,28 +559,13 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
546559
break;
547560
}
548561
if (!\$hasRequiredScheme) {
549-
if ('GET' !== \$canonicalMethod) {
550-
break;
551-
}
552-
553-
return \$this->redirect(\$rawPathinfo, \$ret['_route'], key(\$requiredSchemes)) + \$ret;
554-
}
555-
556-
return \$ret;
557-
558-
EOF;
559-
} else {
560-
$code .= <<<EOF
561-
562-
if (\$requiredMethods && !isset(\$requiredMethods[\$canonicalMethod]) && !isset(\$requiredMethods[\$requestMethod])) {
563-
\$allow += \$requiredMethods;
562+
\$allowSchemes += \$requiredSchemes;
564563
break;
565564
}
566565
567566
return \$ret;
568567
569568
EOF;
570-
}
571569

572570
return $code;
573571
}
@@ -647,9 +645,6 @@ private function compileRoute(Route $route, string $name, bool $checkHost): stri
647645
}
648646

649647
if ($schemes = $route->getSchemes()) {
650-
if (!$this->supportsRedirections) {
651-
throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
652-
}
653648
$schemes = self::export(array_flip($schemes));
654649
if ($methods) {
655650
$code .= <<<EOF
@@ -662,11 +657,8 @@ private function compileRoute(Route $route, string $name, bool $checkHost): stri
662657
goto $gotoname;
663658
}
664659
if (!\$hasRequiredScheme) {
665-
if ('GET' !== \$canonicalMethod) {
666-
goto $gotoname;
667-
}
668-
669-
return \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)) + \$ret;
660+
\$allowSchemes += \$requiredSchemes;
661+
goto $gotoname;
670662
}
671663
672664
@@ -675,11 +667,8 @@ private function compileRoute(Route $route, string $name, bool $checkHost): stri
675667
$code .= <<<EOF
676668
\$requiredSchemes = $schemes;
677669
if (!isset(\$requiredSchemes[\$context->getScheme()])) {
678-
if ('GET' !== \$canonicalMethod) {
679-
goto $gotoname;
680-
}
681-
682-
return \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)) + \$ret;
670+
\$allowSchemes += \$requiredSchemes;
671+
goto $gotoname;
683672
}
684673
685674

src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php

+29-27
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
namespace Symfony\Component\Routing\Matcher;
1313

14+
use Symfony\Component\Routing\Exception\ExceptionInterface;
1415
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
15-
use Symfony\Component\Routing\Route;
1616

1717
/**
1818
* @author Fabien Potencier <fabien@symfony.com>
@@ -27,38 +27,40 @@ public function match($pathinfo)
2727
try {
2828
return parent::match($pathinfo);
2929
} catch (ResourceNotFoundException $e) {
30-
if ('/' === $pathinfo || !\in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
30+
if (!\in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
3131
throw $e;
3232
}
3333

34-
try {
35-
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
36-
$ret = parent::match($pathinfo);
34+
if ($this->allowSchemes) {
35+
redirect_scheme:
36+
$scheme = $this->context->getScheme();
37+
$this->context->setScheme(current($this->allowSchemes));
38+
try {
39+
$ret = parent::match($pathinfo);
3740

38-
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
39-
} catch (ResourceNotFoundException $e2) {
40-
throw $e;
41-
}
42-
}
43-
}
41+
return $this->redirect($pathinfo, $ret['_route'] ?? null, $this->context->getScheme()) + $ret;
42+
} catch (ExceptionInterface $e2) {
43+
throw $e;
44+
} finally {
45+
$this->context->setScheme($scheme);
46+
}
47+
} else {
48+
if ('/' === $pathinfo) {
49+
throw $e;
50+
}
4451

45-
/**
46-
* {@inheritdoc}
47-
*/
48-
protected function handleRouteRequirements($pathinfo, $name, Route $route)
49-
{
50-
// expression condition
51-
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) {
52-
return array(self::REQUIREMENT_MISMATCH, null);
53-
}
52+
try {
53+
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
54+
$ret = parent::match($pathinfo);
5455

55-
// check HTTP scheme requirement
56-
$scheme = $this->context->getScheme();
57-
$schemes = $route->getSchemes();
58-
if ($schemes && !$route->hasScheme($scheme)) {
59-
return array(self::ROUTE_MATCH, $this->redirect($pathinfo, $name, current($schemes)));
56+
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
57+
} catch (ExceptionInterface $e2) {
58+
if ($this->allowSchemes) {
59+
goto redirect_scheme;
60+
}
61+
throw $e;
62+
}
63+
}
6064
}
61-
62-
return array(self::REQUIREMENT_MATCH, null);
6365
}
6466
}

src/Symfony/Component/Routing/Matcher/UrlMatcher.php

+25-9
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,19 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
3333
const ROUTE_MATCH = 2;
3434

3535
protected $context;
36+
37+
/**
38+
* Collects HTTP methods that would be allowed for the request.
39+
*/
3640
protected $allow = array();
41+
42+
/**
43+
* Collects URI schemes that would be allowed for the request.
44+
*
45+
* @internal
46+
*/
47+
protected $allowSchemes = array();
48+
3749
protected $routes;
3850
protected $request;
3951
protected $expressionLanguage;
@@ -70,7 +82,7 @@ public function getContext()
7082
*/
7183
public function match($pathinfo)
7284
{
73-
$this->allow = array();
85+
$this->allow = $this->allowSchemes = array();
7486

7587
if ($ret = $this->matchCollection(rawurldecode($pathinfo), $this->routes)) {
7688
return $ret;
@@ -141,23 +153,31 @@ protected function matchCollection($pathinfo, RouteCollection $routes)
141153
continue;
142154
}
143155

144-
// check HTTP method requirement
156+
$hasRequiredScheme = !$route->getSchemes() || $route->hasScheme($this->context->getScheme());
145157
if ($requiredMethods = $route->getMethods()) {
146158
// HEAD and GET are equivalent as per RFC
147159
if ('HEAD' === $method = $this->context->getMethod()) {
148160
$method = 'GET';
149161
}
150162

151163
if (!in_array($method, $requiredMethods)) {
152-
if (self::REQUIREMENT_MATCH === $status[0]) {
164+
if ($hasRequiredScheme) {
153165
$this->allow = array_merge($this->allow, $requiredMethods);
154166
}
155167

156168
continue;
157169
}
158170
}
159171

160-
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : array()));
172+
$ret = $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : array()));
173+
174+
if (!$hasRequiredScheme) {
175+
$this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
176+
177+
continue;
178+
}
179+
180+
return $ret;
161181
}
162182
}
163183

@@ -197,11 +217,7 @@ protected function handleRouteRequirements($pathinfo, $name, Route $route)
197217
return array(self::REQUIREMENT_MISMATCH, null);
198218
}
199219

200-
// check HTTP scheme requirement
201-
$scheme = $this->context->getScheme();
202-
$status = $route->getSchemes() && !$route->hasScheme($scheme) ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH;
203-
204-
return array($status, null);
220+
return array(self::REQUIREMENT_MATCH, null);
205221
}
206222

207223
/**

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher0.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function __construct(RequestContext $context)
1717

1818
public function match($rawPathinfo)
1919
{
20-
$allow = array();
20+
$allow = $allowSchemes = array();
2121
$pathinfo = rawurldecode($rawPathinfo);
2222
$context = $this->context;
2323
$requestMethod = $canonicalMethod = $context->getMethod();

src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php

+17-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function __construct(RequestContext $context)
1717

1818
public function match($rawPathinfo)
1919
{
20-
$allow = array();
20+
$allow = $allowSchemes = array();
2121
$pathinfo = rawurldecode($rawPathinfo);
2222
$context = $this->context;
2323
$requestMethod = $canonicalMethod = $context->getMethod();
@@ -64,8 +64,15 @@ public function match($rawPathinfo)
6464
}
6565
}
6666

67+
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
6768
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
68-
$allow += $requiredMethods;
69+
if ($hasRequiredScheme) {
70+
$allow += $requiredMethods;
71+
}
72+
break;
73+
}
74+
if (!$hasRequiredScheme) {
75+
$allowSchemes += $requiredSchemes;
6976
break;
7077
}
7178

@@ -209,8 +216,15 @@ public function match($rawPathinfo)
209216
}
210217
}
211218

219+
$hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]);
212220
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
213-
$allow += $requiredMethods;
221+
if ($hasRequiredScheme) {
222+
$allow += $requiredMethods;
223+
}
224+
break;
225+
}
226+
if (!$hasRequiredScheme) {
227+
$allowSchemes += $requiredSchemes;
214228
break;
215229
}
216230

0 commit comments

Comments
 (0)