Commit f998e96f authored by Benni Mack's avatar Benni Mack Committed by Andreas Fernandez
Browse files

[!!!][TASK] Remove Pseudo-Site Handling

This change removes the compatibility layer of Site Handling,
called "PseudoSite" handling.

Any TypoScript-related Language properties are removed.
- config.sys_language_uid
- config.sys_language_mode
- config.sys_language_overlay
- config.locale_all
- config.language
- config.typolinkEnableLinksAcrossDomains
- typolink.useCacheHash

The hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tstemplate.php']['linkData-PostProc']
is removed.

In addition, all tests related to PseudoSite and linking
without SiteHandling are removed, linking to pages without
a site will not be linked anymore.

Adding `useCacheHash` to typolink triggers a
"this does not do anything anymore" deprecation message.

Further related removals (old "pageNotFound" handling
and "useCacheHash" in all viewhelpers), are removed
separately.

Resolves: #88363
Releases: master
Change-Id: I14f2f854e69c98df7fab8b14f92f1ec2440a15a0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/59366

Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
parent 18411d12
......@@ -28,7 +28,6 @@ use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Site\PseudoSiteFinder;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
......@@ -400,14 +399,7 @@ class TreeController
$site = $siteFinder->getSiteByRootPageId($pageId);
$domain = (string)$site->getBase();
} catch (SiteNotFoundException $e) {
// No site found, let's see if it is a legacy-pseudo-site
$pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
try {
$site = $pseudoSiteFinder->getSiteByRootPageId($pageId);
$domain = trim((string)$site->getBase(), '/');
} catch (SiteNotFoundException $e) {
// No pseudo-site found either
}
// No site found
}
return $domain;
......
......@@ -220,12 +220,17 @@ class InputSlugElement extends AbstractFormElement
*/
protected function getPrefix(SiteInterface $site, int $requestLanguageId = 0): string
{
$language = ($requestLanguageId < 0) ? $site->getDefaultLanguage() : $site->getLanguageById($requestLanguageId);
$base = $language->getBase();
$baseUrl = (string)$base;
$baseUrl = rtrim($baseUrl, '/');
if (!empty($baseUrl) && empty($base->getScheme()) && $base->getHost() !== '') {
$baseUrl = 'http:' . $baseUrl;
try {
$language = ($requestLanguageId < 0) ? $site->getDefaultLanguage() : $site->getLanguageById($requestLanguageId);
$base = $language->getBase();
$baseUrl = (string)$base;
$baseUrl = rtrim($baseUrl, '/');
if (!empty($baseUrl) && empty($base->getScheme()) && $base->getHost() !== '') {
$baseUrl = 'http:' . $baseUrl;
}
} catch (\InvalidArgumentException $e) {
// No site found
$baseUrl = '';
}
return $baseUrl;
}
......
......@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Backend\Tree\View;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\PseudoSiteFinder;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -160,15 +159,7 @@ class BrowseTreeView extends AbstractTreeView
$site = $siteFinder->getSiteByRootPageId($pageId);
$title .= ' [' . (string)$site->getBase() . ']';
} catch (SiteNotFoundException $e) {
// No site found, let's see if it is a legacy-pseudo-site
$pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
try {
$site = $pseudoSiteFinder->getSiteByRootPageId($pageId);
$title .= ' [' . trim((string)$site->getBase(), '/') . ']';
} catch (SiteNotFoundException $e) {
// No pseudo-site found either
}
// No site found
}
}
return $title;
......
......@@ -30,7 +30,6 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
......@@ -40,8 +39,6 @@ use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Routing\SiteMatcher;
use TYPO3\CMS\Core\Site\Entity\PseudoSite;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
......@@ -2564,19 +2561,6 @@ class BackendUtility
$domainName = $previewDomainConfig;
}
}
if ($domainName === null) {
// Fetch the "sys_domain" record: First, check for the given domain,
// and find the "root page" = PseudoSite to that domain, then fetch the first
// available sys_domain record.
$siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class);
$result = $siteMatcher->matchRequest(new ServerRequest($domain));
$site = $result->getSite();
if ($site instanceof PseudoSite) {
$domainName = (string)$site->getBase();
$domainName = ltrim($domainName, '/');
}
}
if ($domainName) {
$domain = $protocol . '://' . $domainName;
}
......
......@@ -58,25 +58,4 @@ class InputSlugElementTest extends UnitTestCase
static::assertSame('/en', $subject->_call('getPrefix', $site, 0));
static::assertSame('/de', $subject->_call('getPrefix', $site, 1));
}
/**
* @test
*/
public function getPrefixThrowsInvalidArgumentExceptionForUndefinedLanguages(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(1522960188);
$site = new Site('www.foo.de', 0, []);
$subject = $this->getAccessibleMock(
InputSlugElement::class,
['dummy'],
[],
'',
false
);
$subject->_call('getPrefix', $site, 99);
}
}
......@@ -16,7 +16,8 @@ namespace TYPO3\CMS\Core\Compatibility;
*/
use TYPO3\CMS\Core\Routing\SiteMatcher;
use TYPO3\CMS\Core\Site\Entity\PseudoSite;
use TYPO3\CMS\Core\Site\Entity\NullSite;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
......@@ -77,7 +78,7 @@ class PseudoSiteTcaDisplayCondition
// If not a Site or a NullSite object, it must be a PseudoSite. We show the slug for
// NullSites (new pages below root) to simplify the editing workflow a bit.
$site = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId($defaultLanguagePageId);
$isInPseudoSite = ($site instanceof PseudoSite);
$isInPseudoSite = !($site instanceof Site || $site instanceof NullSite);
if ($parameters['conditionParameters'][1] === 'false') {
// Negate if requested
......
......@@ -23,76 +23,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class LanguageAspectFactory
{
/**
* Create a language aspect based on TypoScript settings, which we know:
*
* config.sys_language_uid
* config.sys_language_mode
* config.sys_language_overlay
*
* @param array $config
* @return LanguageAspect
*/
public static function createFromTypoScript(array $config): LanguageAspect
{
// Get values from TypoScript, if not set before
$languageId = (int)($config['sys_language_uid'] ?? 0);
$fallbacks = GeneralUtility::trimExplode(';', $config['sys_language_mode'] ?? '');
$fallbackMode = null;
if (isset($fallbacks[0])) {
$fallbackMode = $fallbacks[0];
}
$fallbackOrder = null;
if (isset($fallbacks[1])) {
$fallbackOrder = $fallbacks[1];
}
// Page resolving
switch ($fallbackMode) {
case 'strict':
$fallBackOrder = [];
break;
// Ignore anything if a page cannot be found, and resolve pageId=0 instead.
case 'ignore':
$fallBackOrder = [-1];
break;
case 'fallback':
case 'content_fallback':
if (!empty($fallbackOrder)) {
$fallBackOrder = GeneralUtility::trimExplode(',', $fallbackOrder);
// no strict typing explictly done here
if (!in_array(0, $fallBackOrder) && !in_array('pageNotFound', $fallBackOrder)) {
$fallBackOrder[] = 'pageNotFound';
}
} else {
$fallBackOrder = [0];
}
break;
case '':
$fallBackOrder = ['off'];
break;
default:
$fallBackOrder = [0];
}
// Content fetching
switch ((string)($config['sys_language_overlay'] ?? '')) {
case '1':
$overlayType = LanguageAspect::OVERLAYS_MIXED;
break;
case '0':
$overlayType = LanguageAspect::OVERLAYS_OFF;
break;
case 'hideNonTranslated':
$overlayType = LanguageAspect::OVERLAYS_ON;
break;
default:
$overlayType = LanguageAspect::OVERLAYS_ON_WITH_FLOATING;
}
return GeneralUtility::makeInstance(LanguageAspect::class, $languageId, $languageId, $overlayType, $fallBackOrder);
}
/**
* Site Languages always run with overlays + floating records.
*
......
......@@ -117,6 +117,20 @@ class PageRouter implements RouterInterface
if (!($previousResult instanceof RouteResultInterface)) {
throw new RouteNotFoundException('No previous result given. Cannot find a page for an empty route part', 1555303496);
}
// Legacy URIs (?id=12345) takes precedence, no matter if a route is given
$requestId = (string)($request->getQueryParams()['id'] ?? '');
if (!empty($requestId)) {
if (!empty($page = $this->resolvePageId($requestId))) {
return new PageArguments(
(int)($page['l10n_parent'] ?: $page['uid']),
(string)($request->getQueryParams()['type'] ?? '0'),
[],
[],
$request->getQueryParams()
);
}
throw new RouteNotFoundException('The requested page does not exist.', 1557839801);
}
$urlPath = $previousResult->getTail();
// Remove the script name (e.g. index.php), if given
if (!empty($urlPath)) {
......@@ -387,6 +401,38 @@ class PageRouter implements RouterInterface
return $pages;
}
/**
* @param string $pageId
* @return array|null
*/
protected function resolvePageId(string $pageId): ?array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
$queryBuilder
->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class));
$statement = $queryBuilder
->select('uid', 'l10n_parent', 'pid')
->from('pages')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
)
)
->execute();
$page = $statement->fetch();
if (empty($page)) {
return null;
}
return $page;
}
/**
* Fetch possible enhancers + aspects based on the current page configuration and the site configuration put
* into "routeEnhancers"
......
......@@ -24,17 +24,16 @@ use Symfony\Component\Routing\RequestContext;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Site\Entity\PseudoSite;
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\PseudoSiteFinder;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
/**
* Returns a site or pseudo-site (with sys_domain records) based on a given request.
* 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.
......@@ -55,19 +54,13 @@ class SiteMatcher implements SingletonInterface
protected $finder;
/**
* @var PseudoSiteFinder
*/
protected $pseudoSiteFinder;
/**
* Injects necessary objects. PseudoSiteFinder is not injectable as this will be become obsolete in the future.
* Injects necessary objects.
*
* @param SiteFinder|null $finder
*/
public function __construct(SiteFinder $finder = null)
{
$this->finder = $finder ?? GeneralUtility::makeInstance(SiteFinder::class);
$this->pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
}
/**
......@@ -82,8 +75,6 @@ class SiteMatcher implements SingletonInterface
/** Ensure root line caches are flushed */
RootlineUtility::purgeCaches();
GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_rootline')->flush();
$this->pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
$this->pseudoSiteFinder->refresh();
}
/**
......@@ -156,41 +147,19 @@ class SiteMatcher implements SingletonInterface
$result['tail']
);
} catch (NoConfigurationException | ResourceNotFoundException $e) {
// No site+language combination found so far
// At this point we discard a possible found site via ?id=123
// Because ?id=123 _can_ only work if the actual domain/site base works
// so www.domain-without-site-configuration/index.php?id=123 (where 123 is a page referring
// to a page within a site configuration will never be resolved here) properly
$site = new NullSite();
}
// At this point we discard a possible found site via ?id=123
// Because ?id=123 _can_ only work if the actual domain/site base works
// so www.domain-without-site-configuration/index.php?id=123 (where 123 is a page referring
// to a page within a site configuration will never be resolved here) properly
$site = null;
}
// Check against any sys_domain records
$collection = $this->getRouteCollectionForVisibleSysDomains();
$context = new RequestContext('/', $request->getMethod(), $request->getUri()->getHost());
$matcher = new UrlMatcher($collection, $context);
try {
$result = $matcher->match($request->getUri()->getPath());
return new SiteRouteResult($request->getUri(), $result['site'], $result['language'], $result['tail']);
} catch (NoConfigurationException | ResourceNotFoundException $e) {
// No domain record found
}
// No domain record found, try resolving "pseudo-site" again
if ($site == null) {
try {
// use the matching "pseudo-site" for $pageId
$site = $this->pseudoSiteFinder->getSiteByPageId((int)$pageId);
} catch (SiteNotFoundException $exception) {
// use the first "pseudo-site" found
$allPseudoSites = $this->pseudoSiteFinder->findAll();
$site = reset($allPseudoSites);
}
}
return new SiteRouteResult($request->getUri(), $site, $language);
}
/**
* If a given page ID is handed in, a Site/PseudoSite/NullSite is returned.
* 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.
......@@ -202,8 +171,7 @@ class SiteMatcher implements SingletonInterface
try {
return $this->finder->getSiteByPageId($pageId, $rootLine);
} catch (SiteNotFoundException $e) {
// Check for a pseudo / null site
return $this->pseudoSiteFinder->getSiteByPageId($pageId, $rootLine);
return new NullSite();
}
}
......@@ -246,42 +214,6 @@ class SiteMatcher implements SingletonInterface
return $this->createRouteCollectionFromGroupedRoutes($groupedRoutes);
}
/**
* Return the page ID (pid) of a sys_domain record, based on a request object, does the infamous
* "recursive domain search", to also detect if the domain is like "abc.def.example.com" even if the
* sys_domain entry is "example.com".
*
* @return RouteCollection
*/
protected function getRouteCollectionForVisibleSysDomains(): RouteCollection
{
$sites = $this->pseudoSiteFinder->findAll();
$groupedRoutes = [];
foreach ($sites as $site) {
if (!$site instanceof PseudoSite) {
continue;
}
foreach ($site->getEntryPoints() as $uri) {
// Site has no sys_domain record, it is not valid for a routing entrypoint, but only available
// via "id" GET parameter which is handled separately
if (!$uri->getHost()) {
continue;
}
$route = new Route(
($uri->getPath() ?: '/') . '{tail}',
['site' => $site, 'language' => null, 'tail' => ''],
array_filter(['tail' => '.*', 'port' => (string)$uri->getPort()]),
['utf8' => true],
$uri->getHost(),
$uri->getScheme()
);
$identifier = 'site_' . $site->getIdentifier() . '_' . (string)$uri;
$groupedRoutes[($uri->getScheme() ?: '-') . ($uri->getHost() ?: '-')][$uri->getPath() ?: '/'][$identifier] = $route;
}
}
return $this->createRouteCollectionFromGroupedRoutes($groupedRoutes);
}
/**
* As the {tail} parameter is greedy, it needs to be ensured that the one with the
* most specific part matches first.
......
......@@ -47,16 +47,26 @@ class NullSite implements SiteInterface
*/
public function __construct(array $languages = null, Uri $baseEntryPoint = null)
{
foreach ($languages ?? [] as $languageConfiguration) {
$languageUid = (int)$languageConfiguration['languageId'];
// Language configuration does not have a base defined
// So the main site base is used (usually done for default languages)
$this->languages[$languageUid] = new SiteLanguage(
$languageUid,
$languageConfiguration['locale'] ?? '',
$baseEntryPoint ?: new Uri('/'),
$languageConfiguration
if (empty($languages)) {
// Create the default language if no language configuration is given
$this->languages[0] = new SiteLanguage(
0,
'',
new Uri('/'),
['enabled' => true]
);
} else {
foreach ($languages ?? [] as $languageConfiguration) {
$languageUid = (int)$languageConfiguration['languageId'];
// Language configuration does not have a base defined
// So the main site base is used (usually done for default languages)
$this->languages[$languageUid] = new SiteLanguage(
$languageUid,
$languageConfiguration['locale'] ?? '',
$baseEntryPoint ?: new Uri('/'),
$languageConfiguration
);
}
}
}
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Site\Entity;
/*
* 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!
*/
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Core\Http\Uri;
/**
* Entity representing a site with legacy configuration (sys_domain) and all available
* languages in the system (sys_language)
* @internal this class will likely be removed in TYPO3 v10.0. Please use SiteMatcher and SiteInterface to work with Sites in your own code.
*/
class PseudoSite extends NullSite implements SiteInterface
{
/**
* @var string[]
*/
protected $entryPoints;
/**
* attached sys_domain records
* @var array
*/
protected $domainRecords = [];
/**
* Sets up a pseudo site object, and its languages and error handlers
*
* @param int $rootPageId
* @param array $configuration
*/
public function __construct(int $rootPageId, array $configuration)
{
$this->rootPageId = $rootPageId;
foreach ($configuration['domains'] ?? [] as $domain) {
if (empty($domain['domainName'] ?? false)) {
continue;
}
$this->domainRecords[] = $domain;
$this->entryPoints[] = new Uri($this->sanitizeBaseUrl($domain['domainName'] ?: ''));
}
if (empty($this->entryPoints)) {
$this->entryPoints = [new Uri('/')];
}
$baseEntryPoint = reset($this->entryPoints);
parent::__construct($configuration['languages'], $baseEntryPoint);
}
/**
* Returns a generic identifier
*
* @return string
*/
public function getIdentifier(): string
{
return '#PSEUDO_' . $this->rootPageId;
}
/**
* Returns the first base URL of this site, falls back to "/"
*/
public function getBase(): UriInterface
{
return $this->entryPoints[0] ?? new Uri('/');
}
/**
* Returns the base URLs of this site, if none given, it's always "/"
*
* @return UriInterface[]
*/
public function getEntryPoints(): array
{
return $this->entryPoints;
}
/**
* Returns the root page ID of this site
*
* @return int
*/
public function getRootPageId(): int
{
return $this->rootPageId;
}
/**
* If a site base contains "/" or "www.domain.com", it is ensured that
* parse_url() can handle this kind of configuration properly.
*