‪TYPO3CMS  ‪main
BestUrlMatcher.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
21 use Symfony\Component\Routing\Matcher\UrlMatcher;
22 use Symfony\Component\Routing\RouteCollection as SymfonyRouteCollection;
24 
28 class ‪BestUrlMatcher extends UrlMatcher
29 {
30  protected function ‪matchCollection(string $pathinfo, SymfonyRouteCollection $routes): array
31  {
32  $matchedRoutes = $this->‪preMatchCollection($pathinfo, $routes);
33  $matches = count($matchedRoutes);
34  if ($matches === 0) {
35  return [];
36  }
37  if ($matches === 1) {
38  return $matchedRoutes[0]->getRouteResult();
39  }
40  usort($matchedRoutes, [$this, 'sortMatchedRoutes']);
41  return array_shift($matchedRoutes)->getRouteResult();
42  }
43 
52  protected function ‪preMatchCollection(string $pathinfo, SymfonyRouteCollection $routes): array
53  {
54  $matchedRoutes = [];
55 
56  // HEAD and GET are equivalent as per RFC
57  $method = $this->context->getMethod();
58  if ($method === 'HEAD') {
59  $method = 'GET';
60  }
61  $supportsTrailingSlash = $method === 'GET' && $this instanceof RedirectableUrlMatcherInterface;
62  $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
63 
64  foreach ($routes as $name => $route) {
65  $compiledRoute = $route->compile();
66  $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/');
67  $requiredMethods = $route->getMethods();
68 
69  // check the static prefix of the URL first. Only use the more expensive preg_match when it matches
70  if ($staticPrefix !== '' && !str_starts_with($trimmedPathinfo, $staticPrefix)) {
71  continue;
72  }
73  $regex = $compiledRoute->getRegex();
74 
75  $pos = strrpos($regex, '$');
76  $hasTrailingSlash = $regex[$pos - 1] === '/';
77  $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash);
78 
79  if (!preg_match($regex, $pathinfo, $matches)) {
80  continue;
81  }
82 
83  $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath());
84 
85  if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) {
86  if ($hasTrailingSlash) {
87  $matches = $m;
88  } else {
89  $hasTrailingVar = false;
90  }
91  }
92 
93  $hostMatches = [];
94  if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
95  continue;
96  }
97 
98  $attributes = $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
99 
100  $status = $this->handleRouteRequirements($pathinfo, $name, $route, $attributes);
101 
102  if ($status[0] === self::REQUIREMENT_MISMATCH) {
103  continue;
104  }
105 
106  if ($pathinfo !== '/' && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
107  if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) {
108  return $this->allow = $this->allowSchemes = [];
109  }
110  continue;
111  }
112 
113  if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) {
114  $this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes());
115  continue;
116  }
117 
118  if ($requiredMethods && !\in_array($method, $requiredMethods)) {
119  $this->allow = array_merge($this->allow, $requiredMethods);
120  continue;
121  }
122 
123  $matchedRoute = GeneralUtility::makeInstance(
124  MatchedRoute::class,
125  $route,
126  array_replace($attributes, $status[1] ?? [])
127  );
128  $matchedRoutes[] = $matchedRoute->withPathMatches($matches)->withHostMatches($hostMatches);
129  }
130 
131  return $matchedRoutes;
132  }
133 
137  protected function ‪sortMatchedRoutes(‪MatchedRoute $a, ‪MatchedRoute $b): int
138  {
139  if ($a->‪getFallbackScore() !== $b->‪getFallbackScore()) {
140  // sort fallbacks to the end
141  return $a->‪getFallbackScore() <=> $b->‪getFallbackScore();
142  }
143  if ($b->‪getHostMatchScore() !== $a->‪getHostMatchScore()) {
144  // sort more specific host matches to the beginning
145  return $b->‪getHostMatchScore() <=> $a->‪getHostMatchScore();
146  }
147  // index `1` refers to the array index containing the corresponding `tail` match
148  // @todo not sure, whether `tail` can be defined generic, it's hard coded in `SiteMatcher`
149  if ($b->‪getPathMatchScore(1) !== $a->‪getPathMatchScore(1)) {
150  return $b->‪getPathMatchScore(1) <=> $a->‪getPathMatchScore(1);
151  }
152  // fallback for behavior prior to issue #93240, using reverse sorted site identifier
153  // (side note: site identifier did not contain any URL relevant information)
154  return $b->‪getSiteIdentifier() <=> $a->‪getSiteIdentifier();
155  }
156 }
‪TYPO3\CMS\Core\Routing\MatchedRoute\getSiteIdentifier
‪getSiteIdentifier()
Definition: MatchedRoute.php:80
‪TYPO3\CMS\Core\Routing
‪TYPO3\CMS\Core\Routing\BestUrlMatcher\matchCollection
‪matchCollection(string $pathinfo, SymfonyRouteCollection $routes)
Definition: BestUrlMatcher.php:30
‪TYPO3\CMS\Core\Routing\BestUrlMatcher
Definition: BestUrlMatcher.php:29
‪TYPO3\CMS\Core\Routing\MatchedRoute
Definition: MatchedRoute.php:27
‪TYPO3\CMS\Core\Routing\MatchedRoute\getFallbackScore
‪getFallbackScore()
Definition: MatchedRoute.php:57
‪TYPO3\CMS\Core\Routing\BestUrlMatcher\sortMatchedRoutes
‪sortMatchedRoutes(MatchedRoute $a, MatchedRoute $b)
Definition: BestUrlMatcher.php:137
‪TYPO3\CMS\Core\Routing\BestUrlMatcher\preMatchCollection
‪list< MatchedRoute > preMatchCollection(string $pathinfo, SymfonyRouteCollection $routes)
Definition: BestUrlMatcher.php:52
‪TYPO3\CMS\Core\Routing\MatchedRoute\getHostMatchScore
‪getHostMatchScore()
Definition: MatchedRoute.php:62
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Routing\MatchedRoute\getPathMatchScore
‪getPathMatchScore(int $index)
Definition: MatchedRoute.php:67