[Routing] Redirect from trailing slash to no-slash when possible

This commit is contained in:
Nicolas Grekas 2018-02-23 11:41:39 +01:00
parent a38cbd08ce
commit 69a4e94130
20 changed files with 387 additions and 428 deletions

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Routing\Matcher\Dumper;
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
@ -28,6 +29,7 @@ class PhpMatcherDumper extends MatcherDumper
{
private $expressionLanguage;
private $signalingException;
private $supportsRedirections;
/**
* @var ExpressionFunctionProviderInterface[]
@ -55,7 +57,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']);
$supportsRedirections = isset($interfaces['Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface']);
$this->supportsRedirections = isset($interfaces[RedirectableUrlMatcherInterface::class]);
return <<<EOF
<?php
@ -75,7 +77,7 @@ class {$options['class']} extends {$options['base_class']}
\$this->context = \$context;
}
{$this->generateMatchMethod($supportsRedirections)}
{$this->generateMatchMethod()}
}
EOF;
@ -89,7 +91,7 @@ EOF;
/**
* Generates the code for the match method implementing UrlMatcherInterface.
*/
private function generateMatchMethod(bool $supportsRedirections): string
private function generateMatchMethod(): string
{
// Group hosts by same-suffix, re-order when possible
$matchHost = false;
@ -104,15 +106,13 @@ EOF;
}
$routes = $matchHost ? $routes->populateCollection(new RouteCollection()) : $this->getRoutes();
$code = rtrim($this->compileRoutes($routes, $supportsRedirections, $matchHost), "\n");
$code = rtrim($this->compileRoutes($routes, $matchHost), "\n");
$fetchHost = $matchHost ? " \$host = strtolower(\$context->getHost());\n" : '';
return <<<EOF
public function match(\$rawPathinfo)
$code = <<<EOF
{
\$allow = array();
\$pathinfo = rawurldecode(\$rawPathinfo);
\$trimmedPathinfo = rtrim(\$pathinfo, '/');
\$context = \$this->context;
\$requestMethod = \$canonicalMethod = \$context->getMethod();
{$fetchHost}
@ -122,25 +122,49 @@ EOF;
$code
throw \$allow ? new MethodNotAllowedException(array_keys(\$allow)) : new ResourceNotFoundException();
}
EOF;
if ($this->supportsRedirections) {
return <<<'EOF'
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
EOF
.$code."\n return null;\n }";
}
return " public function match(\$rawPathinfo)\n".$code."\n throw \$allow ? new MethodNotAllowedException(array_keys(\$allow)) : new ResourceNotFoundException();\n }";
}
/**
* Generates PHP code to match a RouteCollection with all its routes.
*/
private function compileRoutes(RouteCollection $routes, bool $supportsRedirections, bool $matchHost): string
private function compileRoutes(RouteCollection $routes, bool $matchHost): string
{
list($staticRoutes, $dynamicRoutes) = $this->groupStaticRoutes($routes, $supportsRedirections);
list($staticRoutes, $dynamicRoutes) = $this->groupStaticRoutes($routes);
$code = $this->compileStaticRoutes($staticRoutes, $supportsRedirections, $matchHost);
$code = $this->compileStaticRoutes($staticRoutes, $matchHost);
$chunkLimit = count($dynamicRoutes);
while (true) {
try {
$this->signalingException = new \RuntimeException('preg_match(): Compilation failed: regular expression is too large');
$code .= $this->compileDynamicRoutes($dynamicRoutes, $supportsRedirections, $matchHost, $chunkLimit);
$code .= $this->compileDynamicRoutes($dynamicRoutes, $matchHost, $chunkLimit);
break;
} catch (\Exception $e) {
if (1 < $chunkLimit && $this->signalingException === $e) {
@ -163,7 +187,7 @@ EOF;
/**
* Splits static routes from dynamic routes, so that they can be matched first, using a simple switch.
*/
private function groupStaticRoutes(RouteCollection $collection, bool $supportsRedirections): array
private function groupStaticRoutes(RouteCollection $collection): array
{
$staticRoutes = $dynamicRegex = array();
$dynamicRoutes = new RouteCollection();
@ -172,15 +196,9 @@ EOF;
$compiledRoute = $route->compile();
$hostRegex = $compiledRoute->getHostRegex();
$regex = $compiledRoute->getRegex();
if ($hasTrailingSlash = $supportsRedirections && $pos = strpos($regex, '/$')) {
$regex = substr_replace($regex, '/?$', $pos, 2);
}
if (!$compiledRoute->getPathVariables()) {
$host = !$compiledRoute->getHostVariables() ? $route->getHost() : '';
$url = $route->getPath();
if ($hasTrailingSlash) {
$url = rtrim($url, '/');
}
foreach ($dynamicRegex as list($hostRx, $rx)) {
if (preg_match($rx, $url) && (!$host || !$hostRx || preg_match($hostRx, $host))) {
$dynamicRegex[] = array($hostRegex, $regex);
@ -189,7 +207,7 @@ EOF;
}
}
$staticRoutes[$url][$name] = array($hasTrailingSlash, $route);
$staticRoutes[$url][$name] = $route;
} else {
$dynamicRegex[] = array($hostRegex, $regex);
$dynamicRoutes->add($name, $route);
@ -207,60 +225,56 @@ EOF;
*
* @throws \LogicException
*/
private function compileStaticRoutes(array $staticRoutes, bool $supportsRedirections, bool $matchHost): string
private function compileStaticRoutes(array $staticRoutes, bool $matchHost): string
{
if (!$staticRoutes) {
return '';
}
$code = $default = '';
$checkTrailingSlash = false;
foreach ($staticRoutes as $url => $routes) {
if (1 === count($routes)) {
foreach ($routes as $name => list($hasTrailingSlash, $route)) {
foreach ($routes as $name => $route) {
}
if (!$route->getCondition()) {
if (!$supportsRedirections && $route->getSchemes()) {
if (!$this->supportsRedirections && $route->getSchemes()) {
throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
}
$checkTrailingSlash = $checkTrailingSlash || $hasTrailingSlash;
$default .= sprintf(
"%s => array(%s, %s, %s, %s),\n",
self::export($url),
self::export(array('_route' => $name) + $route->getDefaults()),
self::export(!$route->compile()->getHostVariables() ? $route->getHost() : $route->compile()->getHostRegex() ?: null),
self::export(array_flip($route->getMethods()) ?: null),
self::export(array_flip($route->getSchemes()) ?: null).($hasTrailingSlash ? ', true' : '')
self::export(array_flip($route->getSchemes()) ?: null)
);
continue;
}
}
$code .= sprintf(" case %s:\n", self::export($url));
foreach ($routes as $name => list($hasTrailingSlash, $route)) {
$code .= $this->compileRoute($route, $name, $supportsRedirections, $hasTrailingSlash, true);
foreach ($routes as $name => $route) {
$code .= $this->compileRoute($route, $name, true);
}
$code .= " break;\n";
}
$matchedPathinfo = $supportsRedirections ? '$trimmedPathinfo' : '$pathinfo';
if ($default) {
$code .= <<<EOF
default:
\$routes = array(
{$this->indent($default, 4)} );
if (!isset(\$routes[{$matchedPathinfo}])) {
if (!isset(\$routes[\$pathinfo])) {
break;
}
list(\$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes) = \$routes[{$matchedPathinfo}];
{$this->compileSwitchDefault(false, $matchedPathinfo, $matchHost, $supportsRedirections, $checkTrailingSlash)}
list(\$ret, \$requiredHost, \$requiredMethods, \$requiredSchemes) = \$routes[\$pathinfo];
{$this->compileSwitchDefault(false, $matchHost)}
EOF;
}
return sprintf(" switch (%s) {\n%s }\n\n", $matchedPathinfo, $this->indent($code));
return sprintf(" switch (\$pathinfo) {\n%s }\n\n", $this->indent($code));
}
/**
@ -281,7 +295,7 @@ EOF;
* matching-but-failing subpattern is blacklisted by replacing its name by "(*F)", which forces a failure-to-match.
* To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur.
*/
private function compileDynamicRoutes(RouteCollection $collection, bool $supportsRedirections, bool $matchHost, int $chunkLimit): string
private function compileDynamicRoutes(RouteCollection $collection, bool $matchHost, int $chunkLimit): string
{
if (!$collection->all()) {
return '';
@ -293,8 +307,6 @@ EOF;
'default' => '',
'mark' => 0,
'markTail' => 0,
'supportsRedirections' => $supportsRedirections,
'checkTrailingSlash' => false,
'hostVars' => array(),
'vars' => array(),
);
@ -392,7 +404,7 @@ EOF;
{$this->indent($state->default, 4)} );
list(\$ret, \$vars, \$requiredMethods, \$requiredSchemes) = \$routes[\$m];
{$this->compileSwitchDefault(true, '$m', $matchHost, $supportsRedirections, $state->checkTrailingSlash)}
{$this->compileSwitchDefault(true, $matchHost)}
EOF;
}
@ -448,32 +460,28 @@ EOF;
list($name, $regex, $vars, $route) = $route;
$compiledRoute = $route->compile();
$hasTrailingSlash = $state->supportsRedirections && '' !== $regex && '/' === $regex[-1];
if ($compiledRoute->getRegex() === $prevRegex) {
$state->switch = substr_replace($state->switch, $this->compileRoute($route, $name, $state->supportsRedirections, $hasTrailingSlash, false)."\n", -19, 0);
$state->switch = substr_replace($state->switch, $this->compileRoute($route, $name, false)."\n", -19, 0);
continue;
}
$methods = array_flip($route->getMethods());
$hasTrailingSlash = $hasTrailingSlash && (!$methods || isset($methods['GET']));
$state->mark += 3 + $state->markTail + $hasTrailingSlash + strlen($regex) - $prefixLen;
$state->mark += 3 + $state->markTail + strlen($regex) - $prefixLen;
$state->markTail = 2 + strlen($state->mark);
$rx = sprintf('|%s(*:%s)', substr($regex, $prefixLen).($hasTrailingSlash ? '?' : ''), $state->mark);
$rx = sprintf('|%s(*:%s)', substr($regex, $prefixLen), $state->mark);
$code .= "\n .".self::export($rx);
$state->regex .= $rx;
$vars = array_merge($state->hostVars, $vars);
if (!$route->getCondition() && (!is_array($next = $routes[1 + $i] ?? null) || $regex !== $next[1])) {
$prevRegex = null;
$state->checkTrailingSlash = $state->checkTrailingSlash || $hasTrailingSlash;
$state->default .= sprintf(
"%s => array(%s, %s, %s, %s),\n",
$state->mark,
self::export(array('_route' => $name) + $route->getDefaults()),
self::export($vars),
self::export($methods ?: null),
self::export(array_flip($route->getSchemes()) ?: null).($hasTrailingSlash ? ', true' : '')
self::export(array_flip($route->getMethods()) ?: null),
self::export(array_flip($route->getSchemes()) ?: null)
);
} else {
$prevRegex = $compiledRoute->getRegex();
@ -485,7 +493,7 @@ EOF;
$state->switch .= <<<EOF
case {$state->mark}:
{$combine}{$this->compileRoute($route, $name, $state->supportsRedirections, $hasTrailingSlash, false)}
{$combine}{$this->compileRoute($route, $name, false)}
break;
EOF;
@ -498,7 +506,7 @@ EOF;
/**
* A simple helper to compiles the switch's "default" for both static and dynamic routes.
*/
private function compileSwitchDefault(bool $hasVars, string $routesKey, bool $matchHost, bool $supportsRedirections, bool $checkTrailingSlash): string
private function compileSwitchDefault(bool $hasVars, bool $matchHost): string
{
if ($hasVars) {
$code = <<<EOF
@ -527,21 +535,7 @@ EOF;
} else {
$code = '';
}
if ($supportsRedirections && $checkTrailingSlash) {
$code .= <<<EOF
if (empty(\$routes[{$routesKey}][4]) || '/' === \$pathinfo[-1]) {
// no-op
} elseif ('GET' !== \$canonicalMethod) {
\$allow['GET'] = 'GET';
break;
} else {
return array_replace(\$ret, \$this->redirect(\$rawPathinfo.'/', \$ret['_route']));
}
EOF;
}
if ($supportsRedirections) {
if ($this->supportsRedirections) {
$code .= <<<EOF
if (\$requiredSchemes && !isset(\$requiredSchemes[\$context->getScheme()])) {
@ -550,7 +544,7 @@ EOF;
break;
}
return array_replace(\$ret, \$this->redirect(\$rawPathinfo, \$ret['_route'], key(\$requiredSchemes)));
return \$this->redirect(\$rawPathinfo, \$ret['_route'], key(\$requiredSchemes)) + \$ret;
}
EOF;
@ -574,7 +568,7 @@ EOF;
*
* @throws \LogicException
*/
private function compileRoute(Route $route, string $name, bool $supportsRedirections, bool $hasTrailingSlash, bool $checkHost): string
private function compileRoute(Route $route, string $name, bool $checkHost): string
{
$code = '';
$compiledRoute = $route->compile();
@ -582,12 +576,6 @@ EOF;
$matches = (bool) $compiledRoute->getPathVariables();
$hostMatches = (bool) $compiledRoute->getHostVariables();
$methods = array_flip($route->getMethods());
$supportsTrailingSlash = $supportsRedirections && (!$methods || isset($methods['GET']));
if ($hasTrailingSlash && !$supportsTrailingSlash) {
$hasTrailingSlash = false;
$conditions[] = "'/' === \$pathinfo[-1]";
}
if ($route->getCondition()) {
$expression = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request'));
@ -625,18 +613,17 @@ EOF;
// optimize parameters array
if ($matches || $hostMatches) {
$vars = array();
if ($hostMatches && $checkHost) {
$vars[] = '$hostMatches';
}
$vars = array("array('_route' => '$name')");
if ($matches || ($hostMatches && !$checkHost)) {
$vars[] = '$matches';
}
$vars[] = "array('_route' => '$name')";
if ($hostMatches && $checkHost) {
$vars[] = '$hostMatches';
}
$code .= sprintf(
" \$ret = \$this->mergeDefaults(array_replace(%s), %s);\n",
implode(', ', $vars),
" \$ret = \$this->mergeDefaults(%s, %s);\n",
implode(' + ', $vars),
self::export($route->getDefaults())
);
} elseif ($route->getDefaults()) {
@ -645,23 +632,8 @@ EOF;
$code .= sprintf(" \$ret = array('_route' => '%s');\n", $name);
}
if ($hasTrailingSlash) {
$code .= <<<EOF
if ('/' === \$pathinfo[-1]) {
// no-op
} elseif ('GET' !== \$canonicalMethod) {
\$allow['GET'] = 'GET';
goto $gotoname;
} else {
return array_replace(\$ret, \$this->redirect(\$rawPathinfo.'/', '$name'));
}
EOF;
}
if ($schemes = $route->getSchemes()) {
if (!$supportsRedirections) {
if (!$this->supportsRedirections) {
throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
}
$schemes = self::export(array_flip($schemes));
@ -673,7 +645,7 @@ EOF;
goto $gotoname;
}
return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)));
return \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)) + \$ret;
}
@ -694,18 +666,18 @@ EOF;
EOF;
}
if ($hasTrailingSlash || $schemes || $methods) {
if ($schemes || $methods) {
$code .= " return \$ret;\n";
} else {
$code = substr_replace($code, 'return', $retOffset, 6);
}
if ($conditions) {
$code .= " }\n";
} elseif ($hasTrailingSlash || $schemes || $methods) {
} elseif ($schemes || $methods) {
$code .= ' ';
}
if ($hasTrailingSlash || $schemes || $methods) {
if ($schemes || $methods) {
$code .= " $gotoname:\n";
}

View File

@ -180,12 +180,6 @@ class StaticPrefixCollection
break;
}
}
if (1 < $i && '/' === $prefix[$i - 1]) {
--$i;
}
if (null !== $staticLength && 1 < $staticLength && '/' === $prefix[$staticLength - 1]) {
--$staticLength;
}
return array(substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i));
}

View File

@ -25,22 +25,21 @@ abstract class RedirectableUrlMatcher extends UrlMatcher implements Redirectable
public function match($pathinfo)
{
try {
$parameters = parent::match($pathinfo);
return parent::match($pathinfo);
} catch (ResourceNotFoundException $e) {
if ('/' === substr($pathinfo, -1) || !in_array($this->context->getMethod(), array('HEAD', 'GET'))) {
if ('/' === $pathinfo || !\in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
throw $e;
}
try {
$parameters = parent::match($pathinfo.'/');
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
$ret = parent::match($pathinfo);
return array_replace($parameters, $this->redirect($pathinfo.'/', isset($parameters['_route']) ? $parameters['_route'] : null));
return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret;
} catch (ResourceNotFoundException $e2) {
throw $e;
}
}
return $parameters;
}
/**

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
$host = strtolower($context->getHost());
@ -82,41 +81,41 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
.'|/([^/]++)(*:57)'
.'|head/([^/]++)(*:77)'
.')'
.'|/test/([^/]++)(?'
.'|/(*:103)'
.'|/test/([^/]++)/(?'
.'|(*:103)'
.')'
.'|/([\']+)(*:119)'
.'|/a(?'
.'|/b\'b/([^/]++)(?'
.'|/a/(?'
.'|b\'b/([^/]++)(?'
.'|(*:148)'
.'|(*:156)'
.')'
.'|/(.*)(*:170)'
.'|/b\'b/([^/]++)(?'
.'|(*:194)'
.'|(*:202)'
.'|(.*)(*:169)'
.'|b\'b/([^/]++)(?'
.'|(*:192)'
.'|(*:200)'
.')'
.')'
.'|/multi/hello(?:/([^/]++))?(*:238)'
.'|/multi/hello(?:/([^/]++))?(*:236)'
.'|/([^/]++)/b/([^/]++)(?'
.'|(*:269)'
.'|(*:277)'
.'|(*:267)'
.'|(*:275)'
.')'
.'|/aba/([^/]++)(*:299)'
.'|/aba/([^/]++)(*:297)'
.')|(?i:([^\\.]++)\\.example\\.com)(?'
.'|/route1(?'
.'|3/([^/]++)(*:359)'
.'|4/([^/]++)(*:377)'
.'|3/([^/]++)(*:357)'
.'|4/([^/]++)(*:375)'
.')'
.')|(?i:c\\.example\\.com)(?'
.'|/route15/([^/]++)(*:427)'
.'|/route15/([^/]++)(*:425)'
.')|[^/]*+(?'
.'|/route16/([^/]++)(*:462)'
.'|/a(?'
.'|/a\\.\\.\\.(*:483)'
.'|/b(?'
.'|/([^/]++)(*:505)'
.'|/c/([^/]++)(*:524)'
.'|/route16/([^/]++)(*:460)'
.'|/a/(?'
.'|a\\.\\.\\.(*:481)'
.'|b/(?'
.'|([^/]++)(*:502)'
.'|c/([^/]++)(*:520)'
.')'
.')'
.')'
@ -130,10 +129,10 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
$matches = array('foo' => $matches[1] ?? null);
// baz4
return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array());
return $this->mergeDefaults(array('_route' => 'baz4') + $matches, array());
// baz5
$ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array());
$ret = $this->mergeDefaults(array('_route' => 'baz5') + $matches, array());
if (!isset(($a = array('POST' => 0))[$requestMethod])) {
$allow += $a;
goto not_baz5;
@ -143,7 +142,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
not_baz5:
// baz.baz6
$ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array());
$ret = $this->mergeDefaults(array('_route' => 'baz.baz6') + $matches, array());
if (!isset(($a = array('PUT' => 0))[$requestMethod])) {
$allow += $a;
goto not_bazbaz6;
@ -157,7 +156,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
$matches = array('foo' => $matches[1] ?? null);
// foo1
$ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array());
$ret = $this->mergeDefaults(array('_route' => 'foo1') + $matches, array());
if (!isset(($a = array('PUT' => 0))[$requestMethod])) {
$allow += $a;
goto not_foo1;
@ -167,18 +166,18 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
not_foo1:
break;
case 194:
case 192:
$matches = array('foo1' => $matches[1] ?? null);
// foo2
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array());
return $this->mergeDefaults(array('_route' => 'foo2') + $matches, array());
break;
case 269:
case 267:
$matches = array('_locale' => $matches[1] ?? null, 'foo' => $matches[2] ?? null);
// foo3
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array());
return $this->mergeDefaults(array('_route' => 'foo3') + $matches, array());
break;
default:
@ -188,18 +187,18 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
77 => array(array('_route' => 'barhead'), array('foo'), array('GET' => 0), null),
119 => array(array('_route' => 'quoter'), array('quoter'), null, null),
156 => array(array('_route' => 'bar1'), array('bar'), null, null),
170 => array(array('_route' => 'overridden'), array('var'), null, null),
202 => array(array('_route' => 'bar2'), array('bar1'), null, null),
238 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null),
277 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null),
299 => array(array('_route' => 'foo4'), array('foo'), null, null),
359 => array(array('_route' => 'route13'), array('var1', 'name'), null, null),
377 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null),
427 => array(array('_route' => 'route15'), array('name'), null, null),
462 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null),
483 => array(array('_route' => 'a'), array(), null, null),
505 => array(array('_route' => 'b'), array('var'), null, null),
524 => array(array('_route' => 'c'), array('var'), null, null),
169 => array(array('_route' => 'overridden'), array('var'), null, null),
200 => array(array('_route' => 'bar2'), array('bar1'), null, null),
236 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null),
275 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null),
297 => array(array('_route' => 'foo4'), array('foo'), null, null),
357 => array(array('_route' => 'route13'), array('var1', 'name'), null, null),
375 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null),
425 => array(array('_route' => 'route15'), array('name'), null, null),
460 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null),
481 => array(array('_route' => 'a'), array(), null, null),
502 => array(array('_route' => 'b'), array('var'), null, null),
520 => array(array('_route' => 'c'), array('var'), null, null),
);
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@ -218,7 +217,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
return $ret;
}
if (524 === $m) {
if (520 === $m) {
break;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -15,11 +15,26 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$this->context = $context;
}
public function match($rawPathinfo)
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -30,32 +45,34 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$matchedPathinfo = $pathinfo;
$regexList = array(
0 => '{^(?'
.'|/(en|fr)(?'
.'|/admin/post(?'
.'|/?(*:34)'
.'|/new(*:45)'
.'|/(\\d+)(?'
.'|(*:61)'
.'|/edit(*:73)'
.'|/delete(*:87)'
.'|/(en|fr)/(?'
.'|admin/post/(?'
.'|(*:33)'
.'|new(*:43)'
.'|(\\d+)(?'
.'|(*:58)'
.'|/(?'
.'|edit(*:73)'
.'|delete(*:86)'
.')'
.')'
.')'
.'|/blog(?'
.'|/?(*:106)'
.'|/rss\\.xml(*:123)'
.'|/p(?'
.'|age/([^/]++)(*:148)'
.'|osts/([^/]++)(*:169)'
.'|blog/(?'
.'|(*:104)'
.'|rss\\.xml(*:120)'
.'|p(?'
.'|age/([^/]++)(*:144)'
.'|osts/([^/]++)(*:165)'
.')'
.'|/comments/(\\d+)/new(*:197)'
.'|/search(*:212)'
.'|comments/(\\d+)/new(*:192)'
.'|search(*:206)'
.')'
.'|/log(?'
.'|in(*:230)'
.'|out(*:241)'
.'|log(?'
.'|in(*:223)'
.'|out(*:234)'
.')'
.')'
.'|/(en|fr)?(*:260)'
.'|/(en|fr)?(*:253)'
.')$}sD',
);
@ -64,20 +81,20 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
switch ($m = (int) $matches['MARK']) {
default:
$routes = array(
34 => array(array('_route' => 'a', '_locale' => 'en'), array('_locale'), null, null, true),
45 => array(array('_route' => 'b', '_locale' => 'en'), array('_locale'), null, null),
61 => array(array('_route' => 'c', '_locale' => 'en'), array('_locale', 'id'), null, null),
33 => array(array('_route' => 'a', '_locale' => 'en'), array('_locale'), null, null),
43 => array(array('_route' => 'b', '_locale' => 'en'), array('_locale'), null, null),
58 => array(array('_route' => 'c', '_locale' => 'en'), array('_locale', 'id'), null, null),
73 => array(array('_route' => 'd', '_locale' => 'en'), array('_locale', 'id'), null, null),
87 => array(array('_route' => 'e', '_locale' => 'en'), array('_locale', 'id'), null, null),
106 => array(array('_route' => 'f', '_locale' => 'en'), array('_locale'), null, null, true),
123 => array(array('_route' => 'g', '_locale' => 'en'), array('_locale'), null, null),
148 => array(array('_route' => 'h', '_locale' => 'en'), array('_locale', 'page'), null, null),
169 => array(array('_route' => 'i', '_locale' => 'en'), array('_locale', 'page'), null, null),
197 => array(array('_route' => 'j', '_locale' => 'en'), array('_locale', 'id'), null, null),
212 => array(array('_route' => 'k', '_locale' => 'en'), array('_locale'), null, null),
230 => array(array('_route' => 'l', '_locale' => 'en'), array('_locale'), null, null),
241 => array(array('_route' => 'm', '_locale' => 'en'), array('_locale'), null, null),
260 => array(array('_route' => 'n', '_locale' => 'en'), array('_locale'), null, null),
86 => array(array('_route' => 'e', '_locale' => 'en'), array('_locale', 'id'), null, null),
104 => array(array('_route' => 'f', '_locale' => 'en'), array('_locale'), null, null),
120 => array(array('_route' => 'g', '_locale' => 'en'), array('_locale'), null, null),
144 => array(array('_route' => 'h', '_locale' => 'en'), array('_locale', 'page'), null, null),
165 => array(array('_route' => 'i', '_locale' => 'en'), array('_locale', 'page'), null, null),
192 => array(array('_route' => 'j', '_locale' => 'en'), array('_locale', 'id'), null, null),
206 => array(array('_route' => 'k', '_locale' => 'en'), array('_locale'), null, null),
223 => array(array('_route' => 'l', '_locale' => 'en'), array('_locale'), null, null),
234 => array(array('_route' => 'm', '_locale' => 'en'), array('_locale'), null, null),
253 => array(array('_route' => 'n', '_locale' => 'en'), array('_locale'), null, null),
);
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@ -88,22 +105,13 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
}
if (empty($routes[$m][4]) || '/' === $pathinfo[-1]) {
// no-op
} elseif ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
} else {
return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route']));
}
if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
if ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
}
return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)));
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
@ -114,7 +122,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
return $ret;
}
if (260 === $m) {
if (253 === $m) {
break;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));
@ -122,6 +130,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
return null;
}
}

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -30,19 +29,19 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
$matchedPathinfo = $pathinfo;
$regexList = array(
0 => '{^(?'
.'|/abc([^/]++)(?'
.'|/1(?'
.'|/abc([^/]++)/(?'
.'|1(?'
.'|(*:27)'
.'|0(?'
.'|(*:38)'
.'|0(*:46)'
.')'
.')'
.'|/2(?'
.'|(*:60)'
.'|2(?'
.'|(*:59)'
.'|0(?'
.'|(*:71)'
.'|0(*:79)'
.'|(*:70)'
.'|0(*:78)'
.')'
.')'
.')'
@ -57,9 +56,9 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
27 => array(array('_route' => 'r1'), array('foo'), null, null),
38 => array(array('_route' => 'r10'), array('foo'), null, null),
46 => array(array('_route' => 'r100'), array('foo'), null, null),
60 => array(array('_route' => 'r2'), array('foo'), null, null),
71 => array(array('_route' => 'r20'), array('foo'), null, null),
79 => array(array('_route' => 'r200'), array('foo'), null, null),
59 => array(array('_route' => 'r2'), array('foo'), null, null),
70 => array(array('_route' => 'r20'), array('foo'), null, null),
78 => array(array('_route' => 'r200'), array('foo'), null, null),
);
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@ -78,7 +77,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
return $ret;
}
if (79 === $m) {
if (78 === $m) {
break;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
$host = strtolower($context->getHost());
@ -46,10 +45,10 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
$matches = array('foo' => $matches[1] ?? null, 'foo' => $matches[2] ?? null);
// r1
return $this->mergeDefaults(array_replace($matches, array('_route' => 'r1')), array());
return $this->mergeDefaults(array('_route' => 'r1') + $matches, array());
// r2
return $this->mergeDefaults(array_replace($matches, array('_route' => 'r2')), array());
return $this->mergeDefaults(array('_route' => 'r2') + $matches, array());
break;
}

View File

@ -15,11 +15,26 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$this->context = $context;
}
public function match($rawPathinfo)
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
$host = strtolower($context->getHost());
@ -28,16 +43,16 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$canonicalMethod = 'GET';
}
switch ($trimmedPathinfo) {
switch ($pathinfo) {
default:
$routes = array(
'/test/baz' => array(array('_route' => 'baz'), null, null, null),
'/test/baz.html' => array(array('_route' => 'baz2'), null, null, null),
'/test/baz3' => array(array('_route' => 'baz3'), null, null, null, true),
'/test/baz3/' => array(array('_route' => 'baz3'), null, null, null),
'/foofoo' => array(array('_route' => 'foofoo', 'def' => 'test'), null, null, null),
'/spa ce' => array(array('_route' => 'space'), null, null, null),
'/multi/new' => array(array('_route' => 'overridden2'), null, null, null),
'/multi/hey' => array(array('_route' => 'hey'), null, null, null, true),
'/multi/hey/' => array(array('_route' => 'hey'), null, null, null),
'/ababa' => array(array('_route' => 'ababa'), null, null, null),
'/route1' => array(array('_route' => 'route1'), 'a.example.com', null, null),
'/c2/route2' => array(array('_route' => 'route2'), 'a.example.com', null, null),
@ -52,10 +67,10 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
'/nonsecure' => array(array('_route' => 'nonsecure'), null, null, array('http' => 0)),
);
if (!isset($routes[$trimmedPathinfo])) {
if (!isset($routes[$pathinfo])) {
break;
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$trimmedPathinfo];
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo];
if ($requiredHost) {
if ('#' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) {
@ -67,22 +82,13 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
}
if (empty($routes[$trimmedPathinfo][4]) || '/' === $pathinfo[-1]) {
// no-op
} elseif ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
} else {
return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route']));
}
if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
if ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
}
return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)));
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
@ -102,41 +108,41 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
.'|/([^/]++)(*:57)'
.'|head/([^/]++)(*:77)'
.')'
.'|/test/([^/]++)(?'
.'|/?(*:104)'
.'|/test/([^/]++)/(?'
.'|(*:103)'
.')'
.'|/([\']+)(*:120)'
.'|/a(?'
.'|/b\'b/([^/]++)(?'
.'|(*:149)'
.'|(*:157)'
.'|/([\']+)(*:119)'
.'|/a/(?'
.'|b\'b/([^/]++)(?'
.'|(*:148)'
.'|(*:156)'
.')'
.'|/(.*)(*:171)'
.'|/b\'b/([^/]++)(?'
.'|(*:195)'
.'|(*:203)'
.'|(.*)(*:169)'
.'|b\'b/([^/]++)(?'
.'|(*:192)'
.'|(*:200)'
.')'
.')'
.'|/multi/hello(?:/([^/]++))?(*:239)'
.'|/multi/hello(?:/([^/]++))?(*:236)'
.'|/([^/]++)/b/([^/]++)(?'
.'|(*:270)'
.'|(*:278)'
.'|(*:267)'
.'|(*:275)'
.')'
.'|/aba/([^/]++)(*:300)'
.'|/aba/([^/]++)(*:297)'
.')|(?i:([^\\.]++)\\.example\\.com)(?'
.'|/route1(?'
.'|3/([^/]++)(*:360)'
.'|4/([^/]++)(*:378)'
.'|3/([^/]++)(*:357)'
.'|4/([^/]++)(*:375)'
.')'
.')|(?i:c\\.example\\.com)(?'
.'|/route15/([^/]++)(*:428)'
.'|/route15/([^/]++)(*:425)'
.')|[^/]*+(?'
.'|/route16/([^/]++)(*:463)'
.'|/a(?'
.'|/a\\.\\.\\.(*:484)'
.'|/b(?'
.'|/([^/]++)(*:506)'
.'|/c/([^/]++)(*:525)'
.'|/route16/([^/]++)(*:460)'
.'|/a/(?'
.'|a\\.\\.\\.(*:481)'
.'|b/(?'
.'|([^/]++)(*:502)'
.'|c/([^/]++)(*:520)'
.')'
.')'
.')'
@ -146,53 +152,38 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
foreach ($regexList as $offset => $regex) {
while (preg_match($regex, $matchedPathinfo, $matches)) {
switch ($m = (int) $matches['MARK']) {
case 104:
case 103:
$matches = array('foo' => $matches[1] ?? null);
// baz4
$ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array());
if ('/' === $pathinfo[-1]) {
// no-op
} elseif ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
goto not_baz4;
} else {
return array_replace($ret, $this->redirect($rawPathinfo.'/', 'baz4'));
return $this->mergeDefaults(array('_route' => 'baz4') + $matches, array());
// baz5
$ret = $this->mergeDefaults(array('_route' => 'baz5') + $matches, array());
if (!isset(($a = array('POST' => 0))[$requestMethod])) {
$allow += $a;
goto not_baz5;
}
return $ret;
not_baz4:
// baz5
if ('/' === $pathinfo[-1]) {
$ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array());
if (!isset(($a = array('POST' => 0))[$requestMethod])) {
$allow += $a;
goto not_baz5;
}
return $ret;
}
not_baz5:
// baz.baz6
if ('/' === $pathinfo[-1]) {
$ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array());
if (!isset(($a = array('PUT' => 0))[$requestMethod])) {
$allow += $a;
goto not_bazbaz6;
}
return $ret;
$ret = $this->mergeDefaults(array('_route' => 'baz.baz6') + $matches, array());
if (!isset(($a = array('PUT' => 0))[$requestMethod])) {
$allow += $a;
goto not_bazbaz6;
}
return $ret;
not_bazbaz6:
break;
case 149:
case 148:
$matches = array('foo' => $matches[1] ?? null);
// foo1
$ret = $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array());
$ret = $this->mergeDefaults(array('_route' => 'foo1') + $matches, array());
if (!isset(($a = array('PUT' => 0))[$requestMethod])) {
$allow += $a;
goto not_foo1;
@ -202,18 +193,18 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
not_foo1:
break;
case 195:
case 192:
$matches = array('foo1' => $matches[1] ?? null);
// foo2
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array());
return $this->mergeDefaults(array('_route' => 'foo2') + $matches, array());
break;
case 270:
case 267:
$matches = array('_locale' => $matches[1] ?? null, 'foo' => $matches[2] ?? null);
// foo3
return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array());
return $this->mergeDefaults(array('_route' => 'foo3') + $matches, array());
break;
default:
@ -221,20 +212,20 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
34 => array(array('_route' => 'foo', 'def' => 'test'), array('bar'), null, null),
57 => array(array('_route' => 'bar'), array('foo'), array('GET' => 0, 'HEAD' => 1), null),
77 => array(array('_route' => 'barhead'), array('foo'), array('GET' => 0), null),
120 => array(array('_route' => 'quoter'), array('quoter'), null, null),
157 => array(array('_route' => 'bar1'), array('bar'), null, null),
171 => array(array('_route' => 'overridden'), array('var'), null, null),
203 => array(array('_route' => 'bar2'), array('bar1'), null, null),
239 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null),
278 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null),
300 => array(array('_route' => 'foo4'), array('foo'), null, null),
360 => array(array('_route' => 'route13'), array('var1', 'name'), null, null),
378 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null),
428 => array(array('_route' => 'route15'), array('name'), null, null),
463 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null),
484 => array(array('_route' => 'a'), array(), null, null),
506 => array(array('_route' => 'b'), array('var'), null, null),
525 => array(array('_route' => 'c'), array('var'), null, null),
119 => array(array('_route' => 'quoter'), array('quoter'), null, null),
156 => array(array('_route' => 'bar1'), array('bar'), null, null),
169 => array(array('_route' => 'overridden'), array('var'), null, null),
200 => array(array('_route' => 'bar2'), array('bar1'), null, null),
236 => array(array('_route' => 'helloWorld', 'who' => 'World!'), array('who'), null, null),
275 => array(array('_route' => 'bar3'), array('_locale', 'bar'), null, null),
297 => array(array('_route' => 'foo4'), array('foo'), null, null),
357 => array(array('_route' => 'route13'), array('var1', 'name'), null, null),
375 => array(array('_route' => 'route14', 'var1' => 'val'), array('var1', 'name'), null, null),
425 => array(array('_route' => 'route15'), array('name'), null, null),
460 => array(array('_route' => 'route16', 'var1' => 'val'), array('name'), null, null),
481 => array(array('_route' => 'a'), array(), null, null),
502 => array(array('_route' => 'b'), array('var'), null, null),
520 => array(array('_route' => 'c'), array('var'), null, null),
);
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@ -251,7 +242,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)));
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
@ -262,7 +253,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
return $ret;
}
if (525 === $m) {
if (520 === $m) {
break;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));
@ -270,6 +261,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
return null;
}
}

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -15,11 +15,26 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$this->context = $context;
}
public function match($rawPathinfo)
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -27,36 +42,27 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$canonicalMethod = 'GET';
}
switch ($trimmedPathinfo) {
switch ($pathinfo) {
default:
$routes = array(
'/a/11' => array(array('_route' => 'a_first'), null, null, null),
'/a/22' => array(array('_route' => 'a_second'), null, null, null),
'/a/333' => array(array('_route' => 'a_third'), null, null, null),
'/a/44' => array(array('_route' => 'a_fourth'), null, null, null, true),
'/a/55' => array(array('_route' => 'a_fifth'), null, null, null, true),
'/a/66' => array(array('_route' => 'a_sixth'), null, null, null, true),
'/nested/group/a' => array(array('_route' => 'nested_a'), null, null, null, true),
'/nested/group/b' => array(array('_route' => 'nested_b'), null, null, null, true),
'/nested/group/c' => array(array('_route' => 'nested_c'), null, null, null, true),
'/slashed/group' => array(array('_route' => 'slashed_a'), null, null, null, true),
'/slashed/group/b' => array(array('_route' => 'slashed_b'), null, null, null, true),
'/slashed/group/c' => array(array('_route' => 'slashed_c'), null, null, null, true),
'/a/44/' => array(array('_route' => 'a_fourth'), null, null, null),
'/a/55/' => array(array('_route' => 'a_fifth'), null, null, null),
'/a/66/' => array(array('_route' => 'a_sixth'), null, null, null),
'/nested/group/a/' => array(array('_route' => 'nested_a'), null, null, null),
'/nested/group/b/' => array(array('_route' => 'nested_b'), null, null, null),
'/nested/group/c/' => array(array('_route' => 'nested_c'), null, null, null),
'/slashed/group/' => array(array('_route' => 'slashed_a'), null, null, null),
'/slashed/group/b/' => array(array('_route' => 'slashed_b'), null, null, null),
'/slashed/group/c/' => array(array('_route' => 'slashed_c'), null, null, null),
);
if (!isset($routes[$trimmedPathinfo])) {
if (!isset($routes[$pathinfo])) {
break;
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$trimmedPathinfo];
if (empty($routes[$trimmedPathinfo][4]) || '/' === $pathinfo[-1]) {
// no-op
} elseif ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
} else {
return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route']));
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo];
if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
if ('GET' !== $canonicalMethod) {
@ -64,7 +70,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)));
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
@ -106,7 +112,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)));
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
@ -125,6 +131,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
return null;
}
}

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -56,17 +55,17 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
$matchedPathinfo = $pathinfo;
$regexList = array(
0 => '{^(?'
.'|/trailing/regex(?'
.'|/no\\-methods/([^/]++)/(*:47)'
.'|/get\\-method/([^/]++)/(*:76)'
.'|/head\\-method/([^/]++)/(*:106)'
.'|/post\\-method/([^/]++)/(*:137)'
.'|/trailing/regex/(?'
.'|no\\-methods/([^/]++)/(*:47)'
.'|get\\-method/([^/]++)/(*:75)'
.'|head\\-method/([^/]++)/(*:104)'
.'|post\\-method/([^/]++)/(*:134)'
.')'
.'|/not\\-trailing/regex(?'
.'|/no\\-methods/([^/]++)(*:190)'
.'|/get\\-method/([^/]++)(*:219)'
.'|/head\\-method/([^/]++)(*:249)'
.'|/post\\-method/([^/]++)(*:279)'
.'|/not\\-trailing/regex/(?'
.'|no\\-methods/([^/]++)(*:187)'
.'|get\\-method/([^/]++)(*:215)'
.'|head\\-method/([^/]++)(*:244)'
.'|post\\-method/([^/]++)(*:273)'
.')'
.')$}sD',
);
@ -77,13 +76,13 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
default:
$routes = array(
47 => array(array('_route' => 'regex_trailing_slash_no_methods'), array('param'), null, null),
76 => array(array('_route' => 'regex_trailing_slash_GET_method'), array('param'), array('GET' => 0), null),
106 => array(array('_route' => 'regex_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
137 => array(array('_route' => 'regex_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
190 => array(array('_route' => 'regex_not_trailing_slash_no_methods'), array('param'), null, null),
219 => array(array('_route' => 'regex_not_trailing_slash_GET_method'), array('param'), array('GET' => 0), null),
249 => array(array('_route' => 'regex_not_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
279 => array(array('_route' => 'regex_not_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
75 => array(array('_route' => 'regex_trailing_slash_GET_method'), array('param'), array('GET' => 0), null),
104 => array(array('_route' => 'regex_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
134 => array(array('_route' => 'regex_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
187 => array(array('_route' => 'regex_not_trailing_slash_no_methods'), array('param'), null, null),
215 => array(array('_route' => 'regex_not_trailing_slash_GET_method'), array('param'), array('GET' => 0), null),
244 => array(array('_route' => 'regex_not_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
273 => array(array('_route' => 'regex_not_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
);
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@ -102,7 +101,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
return $ret;
}
if (279 === $m) {
if (273 === $m) {
break;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));

View File

@ -15,11 +15,26 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$this->context = $context;
}
public function match($rawPathinfo)
public function match($pathinfo)
{
$allow = array();
if ($ret = $this->doMatch($pathinfo, $allow)) {
return $ret;
}
if ('/' !== $pathinfo && in_array($this->context->getMethod(), array('HEAD', 'GET'), true)) {
$pathinfo = '/' !== $pathinfo[-1] ? $pathinfo.'/' : substr($pathinfo, 0, -1);
if ($ret = $this->doMatch($pathinfo)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
}
private function doMatch(string $rawPathinfo, array &$allow = array()): ?array
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
@ -27,32 +42,23 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$canonicalMethod = 'GET';
}
switch ($trimmedPathinfo) {
switch ($pathinfo) {
default:
$routes = array(
'/trailing/simple/no-methods' => array(array('_route' => 'simple_trailing_slash_no_methods'), null, null, null, true),
'/trailing/simple/get-method' => array(array('_route' => 'simple_trailing_slash_GET_method'), null, array('GET' => 0), null, true),
'/trailing/simple/head-method' => array(array('_route' => 'simple_trailing_slash_HEAD_method'), null, array('HEAD' => 0), null, true),
'/trailing/simple/post-method' => array(array('_route' => 'simple_trailing_slash_POST_method'), null, array('POST' => 0), null, true),
'/trailing/simple/no-methods/' => array(array('_route' => 'simple_trailing_slash_no_methods'), null, null, null),
'/trailing/simple/get-method/' => array(array('_route' => 'simple_trailing_slash_GET_method'), null, array('GET' => 0), null),
'/trailing/simple/head-method/' => array(array('_route' => 'simple_trailing_slash_HEAD_method'), null, array('HEAD' => 0), null),
'/trailing/simple/post-method/' => array(array('_route' => 'simple_trailing_slash_POST_method'), null, array('POST' => 0), null),
'/not-trailing/simple/no-methods' => array(array('_route' => 'simple_not_trailing_slash_no_methods'), null, null, null),
'/not-trailing/simple/get-method' => array(array('_route' => 'simple_not_trailing_slash_GET_method'), null, array('GET' => 0), null),
'/not-trailing/simple/head-method' => array(array('_route' => 'simple_not_trailing_slash_HEAD_method'), null, array('HEAD' => 0), null),
'/not-trailing/simple/post-method' => array(array('_route' => 'simple_not_trailing_slash_POST_method'), null, array('POST' => 0), null),
);
if (!isset($routes[$trimmedPathinfo])) {
if (!isset($routes[$pathinfo])) {
break;
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$trimmedPathinfo];
if (empty($routes[$trimmedPathinfo][4]) || '/' === $pathinfo[-1]) {
// no-op
} elseif ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
} else {
return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route']));
}
list($ret, $requiredHost, $requiredMethods, $requiredSchemes) = $routes[$pathinfo];
if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
if ('GET' !== $canonicalMethod) {
@ -60,7 +66,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
break;
}
return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)));
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
@ -74,17 +80,17 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$matchedPathinfo = $pathinfo;
$regexList = array(
0 => '{^(?'
.'|/trailing/regex(?'
.'|/no\\-methods/([^/]++)/?(*:48)'
.'|/get\\-method/([^/]++)/?(*:78)'
.'|/head\\-method/([^/]++)/(*:108)'
.'|/post\\-method/([^/]++)/(*:139)'
.'|/trailing/regex/(?'
.'|no\\-methods/([^/]++)/(*:47)'
.'|get\\-method/([^/]++)/(*:75)'
.'|head\\-method/([^/]++)/(*:104)'
.'|post\\-method/([^/]++)/(*:134)'
.')'
.'|/not\\-trailing/regex(?'
.'|/no\\-methods/([^/]++)(*:192)'
.'|/get\\-method/([^/]++)(*:221)'
.'|/head\\-method/([^/]++)(*:251)'
.'|/post\\-method/([^/]++)(*:281)'
.'|/not\\-trailing/regex/(?'
.'|no\\-methods/([^/]++)(*:187)'
.'|get\\-method/([^/]++)(*:215)'
.'|head\\-method/([^/]++)(*:244)'
.'|post\\-method/([^/]++)(*:273)'
.')'
.')$}sD',
);
@ -94,14 +100,14 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
switch ($m = (int) $matches['MARK']) {
default:
$routes = array(
48 => array(array('_route' => 'regex_trailing_slash_no_methods'), array('param'), null, null, true),
78 => array(array('_route' => 'regex_trailing_slash_GET_method'), array('param'), array('GET' => 0), null, true),
108 => array(array('_route' => 'regex_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
139 => array(array('_route' => 'regex_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
192 => array(array('_route' => 'regex_not_trailing_slash_no_methods'), array('param'), null, null),
221 => array(array('_route' => 'regex_not_trailing_slash_GET_method'), array('param'), array('GET' => 0), null),
251 => array(array('_route' => 'regex_not_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
281 => array(array('_route' => 'regex_not_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
47 => array(array('_route' => 'regex_trailing_slash_no_methods'), array('param'), null, null),
75 => array(array('_route' => 'regex_trailing_slash_GET_method'), array('param'), array('GET' => 0), null),
104 => array(array('_route' => 'regex_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
134 => array(array('_route' => 'regex_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
187 => array(array('_route' => 'regex_not_trailing_slash_no_methods'), array('param'), null, null),
215 => array(array('_route' => 'regex_not_trailing_slash_GET_method'), array('param'), array('GET' => 0), null),
244 => array(array('_route' => 'regex_not_trailing_slash_HEAD_method'), array('param'), array('HEAD' => 0), null),
273 => array(array('_route' => 'regex_not_trailing_slash_POST_method'), array('param'), array('POST' => 0), null),
);
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@ -112,22 +118,13 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
}
if (empty($routes[$m][4]) || '/' === $pathinfo[-1]) {
// no-op
} elseif ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
} else {
return array_replace($ret, $this->redirect($rawPathinfo.'/', $ret['_route']));
}
if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) {
if ('GET' !== $canonicalMethod) {
$allow['GET'] = 'GET';
break;
}
return array_replace($ret, $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)));
return $this->redirect($rawPathinfo, $ret['_route'], key($requiredSchemes)) + $ret;
}
if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) {
@ -138,7 +135,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
return $ret;
}
if (281 === $m) {
if (273 === $m) {
break;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));
@ -146,6 +143,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
}
}
throw $allow ? new MethodNotAllowedException(array_keys($allow)) : new ResourceNotFoundException();
return null;
}
}

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();

View File

@ -19,7 +19,6 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
{
$allow = array();
$pathinfo = rawurldecode($rawPathinfo);
$trimmedPathinfo = rtrim($pathinfo, '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
$host = strtolower($context->getHost());
@ -32,11 +31,11 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
case '/':
// a
if (preg_match('#^(?P<d>[^\\.]++)\\.e\\.c\\.b\\.a$#sDi', $host, $hostMatches)) {
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'a')), array());
return $this->mergeDefaults(array('_route' => 'a') + $hostMatches, array());
}
// c
if (preg_match('#^(?P<e>[^\\.]++)\\.e\\.c\\.b\\.a$#sDi', $host, $hostMatches)) {
return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'c')), array());
return $this->mergeDefaults(array('_route' => 'c') + $hostMatches, array());
}
// b
if ('d.c.b.a' === $host) {

View File

@ -19,14 +19,6 @@ use Symfony\Component\Routing\RequestContext;
class DumpedRedirectableUrlMatcherTest extends RedirectableUrlMatcherTest
{
/**
* @expectedException \Symfony\Component\Routing\Exception\MethodNotAllowedException
*/
public function testRedirectWhenNoSlashForNonSafeMethod()
{
parent::testRedirectWhenNoSlashForNonSafeMethod();
}
protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null)
{
static $i = 0;

View File

@ -48,7 +48,7 @@ EOF
),
<<<EOF
root
/prefix/segment
/prefix/segment/
-> prefix_segment
-> leading_segment
EOF
@ -61,7 +61,7 @@ EOF
),
<<<EOF
root
/prefix/segment
/prefix/segment/
-> prefix_segment
-> leading_segment
EOF
@ -75,7 +75,7 @@ EOF
),
<<<EOF
root
/group
/group/
-> nested_segment
-> some_segment
-> other_segment
@ -92,12 +92,12 @@ EOF
array('/group/ff/', 'ff'),
),
<<<EOF
/group
/group/
-> aa
-> bb
-> cc
root
/group
/group/
-> dd
-> ee
-> ff
@ -118,17 +118,17 @@ EOF
array('/aaa/333/', 'third_aaa'),
),
<<<EOF
/aaa
/aaa/
-> first_aaa
-> second_aaa
-> third_aaa
/prefixed
-> /prefixed/group
/prefixed/
-> /prefixed/group/
-> -> aa
-> -> bb
-> -> cc
-> root
-> /prefixed/group
-> /prefixed/group/
-> -> dd
-> -> ee
-> -> ff

View File

@ -27,6 +27,16 @@ class RedirectableUrlMatcherTest extends UrlMatcherTest
$matcher->match('/foo');
}
public function testRedirectWhenSlash()
{
$coll = new RouteCollection();
$coll->add('foo', new Route('/foo'));
$matcher = $this->getUrlMatcher($coll);
$matcher->expects($this->once())->method('redirect')->will($this->returnValue(array()));
$matcher->match('/foo/');
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/