TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Routing;
|
||||
|
||||
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||
use Symfony\Component\Routing\RouteCollection as SymfonyRouteCollection;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BestUrlMatcher extends UrlMatcher
|
||||
{
|
||||
protected function matchCollection(string $pathinfo, SymfonyRouteCollection $routes): array
|
||||
{
|
||||
$matchedRoutes = $this->preMatchCollection($pathinfo, $routes);
|
||||
$matches = count($matchedRoutes);
|
||||
if ($matches === 0) {
|
||||
return [];
|
||||
}
|
||||
if ($matches === 1) {
|
||||
return $matchedRoutes[0]->getRouteResult();
|
||||
}
|
||||
usort($matchedRoutes, [$this, 'sortMatchedRoutes']);
|
||||
return array_shift($matchedRoutes)->getRouteResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to match a URL with a set of routes.
|
||||
* Basically all code has been duplicated from `UrlMatcher::matchCollection`, the difference is
|
||||
* it does not just return the first match, but return all possible matches for further reduction.
|
||||
*
|
||||
* @param string $pathinfo The path info to be parsed
|
||||
* @return list<MatchedRoute>
|
||||
*/
|
||||
protected function preMatchCollection(string $pathinfo, SymfonyRouteCollection $routes): array
|
||||
{
|
||||
$matchedRoutes = [];
|
||||
|
||||
// HEAD and GET are equivalent as per RFC
|
||||
$method = $this->context->getMethod();
|
||||
if ($method === 'HEAD') {
|
||||
$method = 'GET';
|
||||
}
|
||||
$supportsTrailingSlash = $method === 'GET' && $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\x80-\xFF]+\}/?$#', $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;
|
||||
}
|
||||
|
||||
$attributes = $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
|
||||
|
||||
$status = $this->handleRouteRequirements($pathinfo, $name, $route, $attributes);
|
||||
|
||||
if ($status[0] === self::REQUIREMENT_MISMATCH) {
|
||||
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;
|
||||
}
|
||||
|
||||
$matchedRoute = GeneralUtility::makeInstance(
|
||||
MatchedRoute::class,
|
||||
$route,
|
||||
array_replace($attributes, $status[1] ?? [])
|
||||
);
|
||||
$matchedRoutes[] = $matchedRoute->withPathMatches($matches)->withHostMatches($hostMatches);
|
||||
}
|
||||
|
||||
return $matchedRoutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the best matching route result to the beginning
|
||||
*/
|
||||
protected function sortMatchedRoutes(MatchedRoute $a, MatchedRoute $b): int
|
||||
{
|
||||
if ($a->getFallbackScore() !== $b->getFallbackScore()) {
|
||||
// sort fallbacks to the end
|
||||
return $a->getFallbackScore() <=> $b->getFallbackScore();
|
||||
}
|
||||
if ($b->getHostMatchScore() !== $a->getHostMatchScore()) {
|
||||
// sort more specific host matches to the beginning
|
||||
return $b->getHostMatchScore() <=> $a->getHostMatchScore();
|
||||
}
|
||||
// index `1` refers to the array index containing the corresponding `tail` match
|
||||
// @todo not sure, whether `tail` can be defined generic, it's hard coded in `SiteMatcher`
|
||||
if ($b->getPathMatchScore(1) !== $a->getPathMatchScore(1)) {
|
||||
return $b->getPathMatchScore(1) <=> $a->getPathMatchScore(1);
|
||||
}
|
||||
// fallback for behavior prior to issue #93240, using reverse sorted site identifier
|
||||
// (side note: site identifier did not contain any URL relevant information)
|
||||
return $b->getSiteIdentifier() <=> $a->getSiteIdentifier();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user