You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
9.0 KiB
279 lines
9.0 KiB
<?php |
|
|
|
/* |
|
* This file is part of the Symfony package. |
|
* |
|
* (c) Fabien Potencier <fabien@symfony.com> |
|
* |
|
* For the full copyright and license information, please view the LICENSE |
|
* file that was distributed with this source code. |
|
*/ |
|
|
|
namespace Symfony\Component\Routing\Matcher; |
|
|
|
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; |
|
use Symfony\Component\ExpressionLanguage\ExpressionLanguage; |
|
use Symfony\Component\HttpFoundation\Request; |
|
use Symfony\Component\Routing\Exception\MethodNotAllowedException; |
|
use Symfony\Component\Routing\Exception\NoConfigurationException; |
|
use Symfony\Component\Routing\Exception\ResourceNotFoundException; |
|
use Symfony\Component\Routing\RequestContext; |
|
use Symfony\Component\Routing\Route; |
|
use Symfony\Component\Routing\RouteCollection; |
|
|
|
/** |
|
* UrlMatcher matches URL based on a set of routes. |
|
* |
|
* @author Fabien Potencier <fabien@symfony.com> |
|
*/ |
|
class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface |
|
{ |
|
public const REQUIREMENT_MATCH = 0; |
|
public const REQUIREMENT_MISMATCH = 1; |
|
public const ROUTE_MATCH = 2; |
|
|
|
/** @var RequestContext */ |
|
protected $context; |
|
|
|
/** |
|
* Collects HTTP methods that would be allowed for the request. |
|
*/ |
|
protected $allow = []; |
|
|
|
/** |
|
* Collects URI schemes that would be allowed for the request. |
|
* |
|
* @internal |
|
*/ |
|
protected $allowSchemes = []; |
|
|
|
protected $routes; |
|
protected $request; |
|
protected $expressionLanguage; |
|
|
|
/** |
|
* @var ExpressionFunctionProviderInterface[] |
|
*/ |
|
protected $expressionLanguageProviders = []; |
|
|
|
public function __construct(RouteCollection $routes, RequestContext $context) |
|
{ |
|
$this->routes = $routes; |
|
$this->context = $context; |
|
} |
|
|
|
/** |
|
* {@inheritdoc} |
|
*/ |
|
public function setContext(RequestContext $context) |
|
{ |
|
$this->context = $context; |
|
} |
|
|
|
/** |
|
* {@inheritdoc} |
|
*/ |
|
public function getContext() |
|
{ |
|
return $this->context; |
|
} |
|
|
|
/** |
|
* {@inheritdoc} |
|
*/ |
|
public function match(string $pathinfo) |
|
{ |
|
$this->allow = $this->allowSchemes = []; |
|
|
|
if ($ret = $this->matchCollection(rawurldecode($pathinfo) ?: '/', $this->routes)) { |
|
return $ret; |
|
} |
|
|
|
if ('/' === $pathinfo && !$this->allow && !$this->allowSchemes) { |
|
throw new NoConfigurationException(); |
|
} |
|
|
|
throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); |
|
} |
|
|
|
/** |
|
* {@inheritdoc} |
|
*/ |
|
public function matchRequest(Request $request) |
|
{ |
|
$this->request = $request; |
|
|
|
$ret = $this->match($request->getPathInfo()); |
|
|
|
$this->request = null; |
|
|
|
return $ret; |
|
} |
|
|
|
public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) |
|
{ |
|
$this->expressionLanguageProviders[] = $provider; |
|
} |
|
|
|
/** |
|
* Tries to match a URL with a set of routes. |
|
* |
|
* @param string $pathinfo The path info to be parsed |
|
* |
|
* @return array |
|
* |
|
* @throws NoConfigurationException If no routing configuration could be found |
|
* @throws ResourceNotFoundException If the resource could not be found |
|
* @throws MethodNotAllowedException If the resource was found but the request method is not allowed |
|
*/ |
|
protected function matchCollection(string $pathinfo, RouteCollection $routes) |
|
{ |
|
// HEAD and GET are equivalent as per RFC |
|
if ('HEAD' === $method = $this->context->getMethod()) { |
|
$method = 'GET'; |
|
} |
|
$supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface; |
|
$trimmedPathinfo = rtrim($pathinfo, '/') ?: '/'; |
|
|
|
foreach ($routes as $name => $route) { |
|
$compiledRoute = $route->compile(); |
|
$staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/'); |
|
$requiredMethods = $route->getMethods(); |
|
|
|
// check the static prefix of the URL first. Only use the more expensive preg_match when it matches |
|
if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) { |
|
continue; |
|
} |
|
$regex = $compiledRoute->getRegex(); |
|
|
|
$pos = strrpos($regex, '$'); |
|
$hasTrailingSlash = '/' === $regex[$pos - 1]; |
|
$regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash); |
|
|
|
if (!preg_match($regex, $pathinfo, $matches)) { |
|
continue; |
|
} |
|
|
|
$hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{\w+\}/?$#', $route->getPath()); |
|
|
|
if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) { |
|
if ($hasTrailingSlash) { |
|
$matches = $m; |
|
} else { |
|
$hasTrailingVar = false; |
|
} |
|
} |
|
|
|
$hostMatches = []; |
|
if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { |
|
continue; |
|
} |
|
|
|
$status = $this->handleRouteRequirements($pathinfo, $name, $route); |
|
|
|
if (self::REQUIREMENT_MISMATCH === $status[0]) { |
|
continue; |
|
} |
|
|
|
if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { |
|
if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) { |
|
return $this->allow = $this->allowSchemes = []; |
|
} |
|
continue; |
|
} |
|
|
|
if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) { |
|
$this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes()); |
|
continue; |
|
} |
|
|
|
if ($requiredMethods && !\in_array($method, $requiredMethods)) { |
|
$this->allow = array_merge($this->allow, $requiredMethods); |
|
continue; |
|
} |
|
|
|
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, $status[1] ?? [])); |
|
} |
|
|
|
return []; |
|
} |
|
|
|
/** |
|
* Returns an array of values to use as request attributes. |
|
* |
|
* As this method requires the Route object, it is not available |
|
* in matchers that do not have access to the matched Route instance |
|
* (like the PHP and Apache matcher dumpers). |
|
* |
|
* @return array |
|
*/ |
|
protected function getAttributes(Route $route, string $name, array $attributes) |
|
{ |
|
$defaults = $route->getDefaults(); |
|
if (isset($defaults['_canonical_route'])) { |
|
$name = $defaults['_canonical_route']; |
|
unset($defaults['_canonical_route']); |
|
} |
|
$attributes['_route'] = $name; |
|
|
|
return $this->mergeDefaults($attributes, $defaults); |
|
} |
|
|
|
/** |
|
* Handles specific route requirements. |
|
* |
|
* @return array The first element represents the status, the second contains additional information |
|
*/ |
|
protected function handleRouteRequirements(string $pathinfo, string $name, Route $route) |
|
{ |
|
// expression condition |
|
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) { |
|
return [self::REQUIREMENT_MISMATCH, null]; |
|
} |
|
|
|
return [self::REQUIREMENT_MATCH, null]; |
|
} |
|
|
|
/** |
|
* Get merged default parameters. |
|
* |
|
* @return array |
|
*/ |
|
protected function mergeDefaults(array $params, array $defaults) |
|
{ |
|
foreach ($params as $key => $value) { |
|
if (!\is_int($key) && null !== $value) { |
|
$defaults[$key] = $value; |
|
} |
|
} |
|
|
|
return $defaults; |
|
} |
|
|
|
protected function getExpressionLanguage() |
|
{ |
|
if (null === $this->expressionLanguage) { |
|
if (!class_exists(ExpressionLanguage::class)) { |
|
throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); |
|
} |
|
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); |
|
} |
|
|
|
return $this->expressionLanguage; |
|
} |
|
|
|
/** |
|
* @internal |
|
*/ |
|
protected function createRequest(string $pathinfo): ?Request |
|
{ |
|
if (!class_exists(Request::class)) { |
|
return null; |
|
} |
|
|
|
return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), [], [], [ |
|
'SCRIPT_FILENAME' => $this->context->getBaseUrl(), |
|
'SCRIPT_NAME' => $this->context->getBaseUrl(), |
|
]); |
|
} |
|
}
|
|
|