Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Routing\Matcher; Chris@0: Chris@17: use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; Chris@17: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; Chris@17: use Symfony\Component\HttpFoundation\Request; Chris@0: use Symfony\Component\Routing\Exception\MethodNotAllowedException; Chris@14: use Symfony\Component\Routing\Exception\NoConfigurationException; Chris@0: use Symfony\Component\Routing\Exception\ResourceNotFoundException; Chris@0: use Symfony\Component\Routing\RequestContext; Chris@0: use Symfony\Component\Routing\Route; Chris@17: use Symfony\Component\Routing\RouteCollection; Chris@0: Chris@0: /** Chris@0: * UrlMatcher matches URL based on a set of routes. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface Chris@0: { Chris@0: const REQUIREMENT_MATCH = 0; Chris@0: const REQUIREMENT_MISMATCH = 1; Chris@0: const ROUTE_MATCH = 2; Chris@0: Chris@0: protected $context; Chris@17: protected $allow = []; Chris@0: protected $routes; Chris@0: protected $request; Chris@0: protected $expressionLanguage; Chris@0: Chris@0: /** Chris@0: * @var ExpressionFunctionProviderInterface[] Chris@0: */ Chris@17: protected $expressionLanguageProviders = []; Chris@0: Chris@0: public function __construct(RouteCollection $routes, RequestContext $context) Chris@0: { Chris@0: $this->routes = $routes; Chris@0: $this->context = $context; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setContext(RequestContext $context) Chris@0: { Chris@0: $this->context = $context; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getContext() Chris@0: { Chris@0: return $this->context; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function match($pathinfo) Chris@0: { Chris@17: $this->allow = []; Chris@0: Chris@0: if ($ret = $this->matchCollection(rawurldecode($pathinfo), $this->routes)) { Chris@0: return $ret; Chris@0: } Chris@0: Chris@14: if ('/' === $pathinfo && !$this->allow) { Chris@14: throw new NoConfigurationException(); Chris@14: } Chris@14: Chris@17: throw 0 < \count($this->allow) Chris@0: ? new MethodNotAllowedException(array_unique($this->allow)) Chris@0: : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function matchRequest(Request $request) Chris@0: { Chris@0: $this->request = $request; Chris@0: Chris@0: $ret = $this->match($request->getPathInfo()); Chris@0: Chris@0: $this->request = null; Chris@0: Chris@0: return $ret; Chris@0: } Chris@0: Chris@0: public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) Chris@0: { Chris@0: $this->expressionLanguageProviders[] = $provider; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tries to match a URL with a set of routes. Chris@0: * Chris@0: * @param string $pathinfo The path info to be parsed Chris@0: * @param RouteCollection $routes The set of routes Chris@0: * Chris@0: * @return array An array of parameters Chris@0: * Chris@14: * @throws NoConfigurationException If no routing configuration could be found Chris@0: * @throws ResourceNotFoundException If the resource could not be found Chris@0: * @throws MethodNotAllowedException If the resource was found but the request method is not allowed Chris@0: */ Chris@0: protected function matchCollection($pathinfo, RouteCollection $routes) Chris@0: { Chris@17: // HEAD and GET are equivalent as per RFC Chris@17: if ('HEAD' === $method = $this->context->getMethod()) { Chris@17: $method = 'GET'; Chris@17: } Chris@17: $supportsTrailingSlash = '/' !== $pathinfo && '' !== $pathinfo && $this instanceof RedirectableUrlMatcherInterface; Chris@17: Chris@0: foreach ($routes as $name => $route) { Chris@0: $compiledRoute = $route->compile(); Chris@17: $staticPrefix = $compiledRoute->getStaticPrefix(); Chris@17: $requiredMethods = $route->getMethods(); Chris@0: Chris@0: // check the static prefix of the URL first. Only use the more expensive preg_match when it matches Chris@17: if ('' === $staticPrefix || 0 === strpos($pathinfo, $staticPrefix)) { Chris@17: // no-op Chris@17: } elseif (!$supportsTrailingSlash || ($requiredMethods && !\in_array('GET', $requiredMethods)) || 'GET' !== $method) { Chris@17: continue; Chris@17: } elseif ('/' === substr($staticPrefix, -1) && substr($staticPrefix, 0, -1) === $pathinfo) { Chris@17: return $this->allow = []; Chris@17: } else { Chris@17: continue; Chris@17: } Chris@17: $regex = $compiledRoute->getRegex(); Chris@17: Chris@17: if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) { Chris@17: $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); Chris@17: $hasTrailingSlash = true; Chris@17: } else { Chris@17: $hasTrailingSlash = false; Chris@17: } Chris@17: Chris@17: if (!preg_match($regex, $pathinfo, $matches)) { Chris@0: continue; Chris@0: } Chris@0: Chris@17: if ($hasTrailingSlash && '/' !== substr($pathinfo, -1)) { Chris@17: if ((!$requiredMethods || \in_array('GET', $requiredMethods)) && 'GET' === $method) { Chris@17: return $this->allow = []; Chris@17: } Chris@0: continue; Chris@0: } Chris@0: Chris@17: $hostMatches = []; Chris@0: if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { Chris@0: continue; Chris@0: } Chris@0: Chris@14: $status = $this->handleRouteRequirements($pathinfo, $name, $route); Chris@14: Chris@14: if (self::REQUIREMENT_MISMATCH === $status[0]) { Chris@14: continue; Chris@14: } Chris@14: Chris@0: // check HTTP method requirement Chris@17: if ($requiredMethods) { Chris@17: if (!\in_array($method, $requiredMethods)) { Chris@14: if (self::REQUIREMENT_MATCH === $status[0]) { Chris@14: $this->allow = array_merge($this->allow, $requiredMethods); Chris@14: } Chris@0: Chris@0: continue; Chris@0: } Chris@0: } Chris@0: Chris@17: return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : [])); Chris@0: } Chris@17: Chris@17: return []; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns an array of values to use as request attributes. Chris@0: * Chris@0: * As this method requires the Route object, it is not available Chris@0: * in matchers that do not have access to the matched Route instance Chris@0: * (like the PHP and Apache matcher dumpers). Chris@0: * Chris@0: * @param Route $route The route we are matching against Chris@0: * @param string $name The name of the route Chris@0: * @param array $attributes An array of attributes from the matcher Chris@0: * Chris@0: * @return array An array of parameters Chris@0: */ Chris@0: protected function getAttributes(Route $route, $name, array $attributes) Chris@0: { Chris@0: $attributes['_route'] = $name; Chris@0: Chris@0: return $this->mergeDefaults($attributes, $route->getDefaults()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handles specific route requirements. Chris@0: * Chris@0: * @param string $pathinfo The path Chris@0: * @param string $name The route name Chris@0: * @param Route $route The route Chris@0: * Chris@0: * @return array The first element represents the status, the second contains additional information Chris@0: */ Chris@0: protected function handleRouteRequirements($pathinfo, $name, Route $route) Chris@0: { Chris@0: // expression condition Chris@17: if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) { Chris@17: return [self::REQUIREMENT_MISMATCH, null]; Chris@0: } Chris@0: Chris@0: // check HTTP scheme requirement Chris@0: $scheme = $this->context->getScheme(); Chris@0: $status = $route->getSchemes() && !$route->hasScheme($scheme) ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH; Chris@0: Chris@17: return [$status, null]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get merged default parameters. Chris@0: * Chris@0: * @param array $params The parameters Chris@0: * @param array $defaults The defaults Chris@0: * Chris@0: * @return array Merged default parameters Chris@0: */ Chris@0: protected function mergeDefaults($params, $defaults) Chris@0: { Chris@0: foreach ($params as $key => $value) { Chris@17: if (!\is_int($key)) { Chris@0: $defaults[$key] = $value; Chris@0: } Chris@0: } Chris@0: Chris@0: return $defaults; Chris@0: } Chris@0: Chris@0: protected function getExpressionLanguage() Chris@0: { Chris@0: if (null === $this->expressionLanguage) { Chris@0: if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { Chris@0: throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); Chris@0: } Chris@0: $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); Chris@0: } Chris@0: Chris@0: return $this->expressionLanguage; Chris@0: } Chris@12: Chris@12: /** Chris@12: * @internal Chris@12: */ Chris@12: protected function createRequest($pathinfo) Chris@12: { Chris@12: if (!class_exists('Symfony\Component\HttpFoundation\Request')) { Chris@12: return null; Chris@12: } Chris@12: Chris@17: return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), [], [], [ Chris@12: 'SCRIPT_FILENAME' => $this->context->getBaseUrl(), Chris@12: 'SCRIPT_NAME' => $this->context->getBaseUrl(), Chris@17: ]); Chris@12: } Chris@0: }