276 lines
11 KiB
PHP
276 lines
11 KiB
PHP
<?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 Psr\Http\Message\ServerRequestInterface;
|
|
use Psr\Http\Message\UriInterface;
|
|
use Symfony\Component\Routing\Exception\NoConfigurationException;
|
|
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
|
use TYPO3\CMS\Core\Cache\CacheManager;
|
|
use TYPO3\CMS\Core\Configuration\Features;
|
|
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
|
use TYPO3\CMS\Core\Http\NormalizedParams;
|
|
use TYPO3\CMS\Core\SingletonInterface;
|
|
use TYPO3\CMS\Core\Site\Entity\NullSite;
|
|
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
|
|
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
|
use TYPO3\CMS\Core\Site\SiteFinder;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
use TYPO3\CMS\Core\Utility\RootlineUtility;
|
|
|
|
/**
|
|
* Returns a site based on a given request.
|
|
*
|
|
* The main usage is the ->matchRequest() functionality, which receives a request object and boots up
|
|
* Symfony Routing to find the proper route with its defaults / attributes.
|
|
*
|
|
* On top, this is also commonly used throughout TYPO3 to fetch a site by a given pageId.
|
|
* ->matchPageId().
|
|
*
|
|
* The concept of the SiteMatcher is to *resolve*, and not build URIs. On top, it is a facade to hide the
|
|
* dependency to symfony and to not expose its logic.
|
|
*
|
|
* @internal Please note that the site matcher will be probably cease to exist and adapted to the SiteFinder concept when Pseudo-Site handling will be removed.
|
|
*/
|
|
readonly class SiteMatcher implements SingletonInterface
|
|
{
|
|
public function __construct(
|
|
protected Features $features,
|
|
protected SiteFinder $finder,
|
|
protected RequestContextFactory $requestContextFactory
|
|
) {}
|
|
|
|
/**
|
|
* Only used when a page is moved but the pseudo site caches has this information hard-coded, so the caches
|
|
* need to be flushed.
|
|
*
|
|
* @internal
|
|
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
|
|
*/
|
|
public function refresh()
|
|
{
|
|
/** Ensure root line caches are flushed */
|
|
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
|
|
$cacheManager->getCache('runtime')->flushByTag(RootlineUtility::RUNTIME_CACHE_TAG);
|
|
$cacheManager->getCache('rootline')->flush();
|
|
}
|
|
|
|
/**
|
|
* First, it is checked, if a "id" GET/POST parameter is found.
|
|
* If it is, we check for a valid site mounted there.
|
|
*
|
|
* If it isn't the quest continues by validating the whole request URL and validating against
|
|
* all available site records (and their language prefixes).
|
|
*
|
|
* @param ServerRequestInterface $request
|
|
*/
|
|
public function matchRequest(ServerRequestInterface $request): RouteResultInterface
|
|
{
|
|
// Remove script file name (index.php) from request uri
|
|
$uri = $this->canonicalizeUri($request->getUri(), $request);
|
|
$pageId = $this->resolvePageIdQueryParam($request);
|
|
$languageId = $this->resolveLanguageIdQueryParam($request);
|
|
|
|
$routeResult = $this->matchSiteByUri($uri, $request);
|
|
|
|
// Allow insecure pageId based site resolution if explicitly enabled and only if both, ?id= and ?L= are defined
|
|
// (pageId based site resolution without L parameter has always been prohibited, so we do not support that)
|
|
if (
|
|
$this->features->isFeatureEnabled('security.frontend.allowInsecureSiteResolutionByQueryParameters')
|
|
&& $pageId !== null && $languageId !== null
|
|
) {
|
|
return $this->matchSiteByQueryParams($pageId, $languageId, $routeResult, $uri);
|
|
}
|
|
|
|
// Allow the default language to be resolved in case all languages use a prefix
|
|
// and therefore did not match based on path if an explicit pageId is given,
|
|
// (example "https://www.example.com/?id=.." was entered, but all languages have "https://www.example.com/lang-key/")
|
|
// @todo remove this fallback, in order for SiteBaseRedirectResolver to produce a redirect instead (requires functionals to be adapted)
|
|
if ($pageId !== null && $routeResult->getLanguage() === null) {
|
|
$routeResult = $routeResult->withLanguage($routeResult->getSite()->getDefaultLanguage());
|
|
}
|
|
|
|
// adjust the language aspect if it was given by query param `&L` (and ?id is given)
|
|
// @todo remove, this is added for backwards (and functional tests) compatibility reasons
|
|
if ($languageId !== null && $pageId !== null) {
|
|
try {
|
|
// override/set language by `&L=` query param
|
|
$routeResult = $routeResult->withLanguage($routeResult->getSite()->getLanguageById($languageId));
|
|
} catch (\InvalidArgumentException) {
|
|
// ignore; language id not available
|
|
}
|
|
}
|
|
|
|
return $routeResult;
|
|
}
|
|
|
|
/**
|
|
* If a given page ID is handed in, a Site/NullSite is returned.
|
|
*
|
|
* @param int $pageId uid of a page in default language
|
|
* @param array|null $rootLine an alternative root line, if already at and.
|
|
*/
|
|
public function matchByPageId(int $pageId, ?array $rootLine = null): SiteInterface
|
|
{
|
|
try {
|
|
return $this->finder->getSiteByPageId($pageId, $rootLine);
|
|
} catch (SiteNotFoundException) {
|
|
return new NullSite();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a Symfony RouteCollection containing all routes to all sites.
|
|
*/
|
|
protected function getRouteCollectionForAllSites(): RouteCollection
|
|
{
|
|
$collection = new RouteCollection();
|
|
foreach ($this->finder->getAllSites() as $site) {
|
|
// Add the site as entrypoint
|
|
// @todo Find a way to test only this basic route against chinese characters, as site languages kicking
|
|
// always in. Do the rawurldecode() here to to be consistent with language preparations.
|
|
|
|
$uri = $site->getBase();
|
|
$route = new Route(
|
|
(rawurldecode($uri->getPath()) ?: '/') . '{tail}',
|
|
['site' => $site, 'language' => null, 'tail' => ''],
|
|
array_filter(['tail' => '.*', 'port' => (string)$uri->getPort()]),
|
|
['utf8' => true, 'fallback' => true],
|
|
// @todo Verify if host should here covered with idn_to_ascii() to be consistent with preparation for languages.
|
|
$uri->getHost() ?: '',
|
|
$uri->getScheme() === '' ? [] : [$uri->getScheme()]
|
|
);
|
|
$identifier = 'site_' . $site->getIdentifier();
|
|
$collection->add($identifier, $route);
|
|
|
|
// Add all languages
|
|
foreach ($site->getAllLanguages() as $siteLanguage) {
|
|
$uri = $siteLanguage->getBase();
|
|
$route = new Route(
|
|
(rawurldecode($uri->getPath()) ?: '/') . '{tail}',
|
|
['site' => $site, 'language' => $siteLanguage, 'tail' => ''],
|
|
array_filter(['tail' => '.*', 'port' => (string)$uri->getPort()]),
|
|
['utf8' => true],
|
|
$uri->getHost() ? (string)idn_to_ascii($uri->getHost()) : '',
|
|
$uri->getScheme() === '' ? [] : [$uri->getScheme()]
|
|
);
|
|
$identifier = 'site_' . $site->getIdentifier() . '_' . $siteLanguage->getLanguageId();
|
|
$collection->add($identifier, $route);
|
|
}
|
|
}
|
|
return $collection;
|
|
}
|
|
|
|
/**
|
|
* @return ?positive-int
|
|
*/
|
|
protected function resolvePageIdQueryParam(ServerRequestInterface $request): ?int
|
|
{
|
|
$pageId = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? null;
|
|
if ($pageId === null) {
|
|
return null;
|
|
}
|
|
if (!MathUtility::canBeInterpretedAsInteger($pageId)) {
|
|
return null;
|
|
}
|
|
return (int)$pageId <= 0 ? null : (int)$pageId;
|
|
}
|
|
|
|
/**
|
|
* @return ?positive-int
|
|
*/
|
|
protected function resolveLanguageIdQueryParam(ServerRequestInterface $request): ?int
|
|
{
|
|
$languageId = $request->getQueryParams()['L'] ?? $request->getParsedBody()['L'] ?? null;
|
|
if ($languageId === null) {
|
|
return null;
|
|
}
|
|
if (!MathUtility::canBeInterpretedAsInteger($languageId)) {
|
|
return null;
|
|
}
|
|
return (int)$languageId < 0 ? null : (int)$languageId;
|
|
}
|
|
|
|
/**
|
|
* Remove script file name (index.php) from request uri
|
|
*/
|
|
protected function canonicalizeUri(UriInterface $uri, ServerRequestInterface $request): UriInterface
|
|
{
|
|
if ($uri->getPath() === '') {
|
|
return $uri;
|
|
}
|
|
|
|
$normalizedParams = $request->getAttribute('normalizedParams');
|
|
if (!$normalizedParams instanceof NormalizedParams) {
|
|
return $uri;
|
|
}
|
|
|
|
$urlPath = ltrim($uri->getPath(), '/');
|
|
$scriptName = ltrim($normalizedParams->getScriptName(), '/');
|
|
$scriptPath = ltrim($normalizedParams->getSitePath(), '/');
|
|
if ($scriptName !== '' && str_starts_with($urlPath, $scriptName)) {
|
|
$urlPath = '/' . $scriptPath . substr($urlPath, mb_strlen($scriptName));
|
|
$uri = $uri->withPath($urlPath);
|
|
}
|
|
|
|
return $uri;
|
|
}
|
|
|
|
protected function matchSiteByUri(UriInterface $uri, ServerRequestInterface $request): SiteRouteResult
|
|
{
|
|
$collection = $this->getRouteCollectionForAllSites();
|
|
$requestContext = $this->requestContextFactory->fromUri($uri, $request->getMethod());
|
|
$matcher = new BestUrlMatcher($collection, $requestContext);
|
|
try {
|
|
/** @var array{site: SiteInterface, language: ?SiteLanguage, tail: string} $match */
|
|
$match = $matcher->match($uri->getPath());
|
|
return new SiteRouteResult(
|
|
$uri,
|
|
$match['site'],
|
|
$match['language'],
|
|
$match['tail']
|
|
);
|
|
} catch (NoConfigurationException|ResourceNotFoundException) {
|
|
return new SiteRouteResult($uri, new NullSite(), null, '');
|
|
}
|
|
}
|
|
|
|
protected function matchSiteByQueryParams(
|
|
int $pageId,
|
|
int $languageId,
|
|
SiteRouteResult $fallback,
|
|
UriInterface $uri,
|
|
): SiteRouteResult {
|
|
try {
|
|
$site = $this->finder->getSiteByPageId($pageId);
|
|
} catch (SiteNotFoundException) {
|
|
return $fallback;
|
|
}
|
|
|
|
try {
|
|
// override/set language by `&L=` query param
|
|
$language = $site->getLanguageById($languageId);
|
|
} catch (\InvalidArgumentException) {
|
|
return $fallback;
|
|
}
|
|
|
|
return new SiteRouteResult($uri, $site, $language);
|
|
}
|
|
}
|