From 733da8558a11f0f17c21423fa19f81d36f23fe0c Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Fri, 17 Aug 2018 14:40:56 +0200 Subject: [PATCH] [TASK] Centralize sys_domain resolving Core provides various places to either retrieve the pageId of a sys_domain record, or to find out the root page ID given a request (previously domain + path). However, due to the inclusion of symfony/routing, the detection can now be handled by this new component. For this purpose, a new class LegacyDomainResolver is introduced (internal) which utilizes symfony/routing under the hood and does proper caching. This way, the following methods can be deprecated: - TypoScriptFrontendController->domainNameMatchesCurrentRequest() - TypoScriptFrontendController->getDomainDataForPid() - BackendUtility::getDomainStartPage() - BackendUtility::firstDomainRecord() This is a precursor for fetching all sys_domain data, to minimize the efforts later-on to wrap the LegacyDomainResolver into a proper Router, and also to build pseudo-sites on top. Resolves: #85892 Releases: master Change-Id: I831d33ac06f090cbe4d2a16e592549739489f99a Reviewed-on: https://review.typo3.org/57949 Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de> --- .../Classes/Utility/BackendUtility.php | 54 ++-- ...iousMethodsRegardingSysDomainResolving.rst | 43 +++ .../Compatibility/LegacyDomainResolver.php | 282 ++++++++++++++++++ .../TypoScriptFrontendController.php | 85 +----- .../Classes/Middleware/SiteResolver.php | 77 +---- .../Classes/Typolink/PageLinkBuilder.php | 12 +- .../TypoScriptFrontendControllerTest.php | 78 ----- .../LegacyDomainResolverTest.php | 103 +++++++ .../TypoScriptFrontendControllerTest.php | 58 ---- .../TypoScriptFrontendControllerTest.php | 96 ++++++ .../Php/MethodCallMatcher.php | 14 + .../Php/MethodCallStaticMatcher.php | 14 + 12 files changed, 595 insertions(+), 321 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst create mode 100644 typo3/sysext/frontend/Classes/Compatibility/LegacyDomainResolver.php create mode 100644 typo3/sysext/frontend/Tests/Unit/Compatibility/LegacyDomainResolverTest.php create mode 100644 typo3/sysext/frontend/Tests/UnitDeprecated/Controller/TypoScriptFrontendControllerTest.php diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php index d45bba0629f6..c9cc3fbcab71 100644 --- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php +++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php @@ -30,6 +30,7 @@ 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; @@ -50,6 +51,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Core\Versioning\VersionState; +use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver; use TYPO3\CMS\Frontend\Page\PageRepository; /** @@ -2825,13 +2827,24 @@ class BackendUtility $domainName = $previewDomainConfig; } } else { - $domainName = self::firstDomainRecord($rootLine); + $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class); + foreach ($rootLine as $row) { + $domainRecord = $domainResolver->matchRootPageId($row['uid']); + if (is_array($domainRecord)) { + $domainName = rtrim($domainRecord['domainName'], '/'); + break; + } + } } if ($domainName) { $domain = $domainName; } else { - $domainRecord = self::getDomainStartPage($urlParts['host'], $urlParts['path']); - $domain = $domainRecord['domainName']; + $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class); + $rootPageId = $domainResolver->matchRequest(new ServerRequest($domain)); + if ($rootPageId > 0) { + $domainRecord = $domainResolver->matchRootPageId($rootPageId); + $domain = $domainRecord['domainName']; + } } if ($domain) { $domain = $protocol . '://' . $domain; @@ -3552,37 +3565,16 @@ class BackendUtility * * @param array $rootLine Root line array * @return string|null Domain name or NULL + * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0. Use Link Generation / Router instead. */ public static function firstDomainRecord($rootLine) { - $queryBuilder = static::getQueryBuilderForTable('sys_domain'); - $queryBuilder->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) - ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); - - $queryBuilder->select('domainName') - ->from('sys_domain') - ->where( - $queryBuilder->expr()->eq( - 'pid', - $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT, ':pid') - ), - $queryBuilder->expr()->eq( - 'hidden', - $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) - ) - ) - ->setMaxResults(1) - ->orderBy('sorting'); - + trigger_error('BackendUtility::firstDomainRecord() will be removed in TYPO3 v10.0. Use the new LigetDomainStartPagenk Generation functionality instead.', E_USER_DEPRECATED); + $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class); foreach ($rootLine as $row) { - $domainName = $queryBuilder->setParameter('pid', $row['uid'], \PDO::PARAM_INT) - ->execute() - ->fetchColumn(0); - - if ($domainName) { - return rtrim($domainName, '/'); + $domain = $domainResolver->matchRootPageId($row['uid']); + if (is_array($domain)) { + return rtrim($domain['domainName'], '/'); } } return null; @@ -3594,9 +3586,11 @@ class BackendUtility * @param string $domain Domain name * @param string $path Appended path * @return array|bool Domain record, if found, false otherwise + * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0. Use Link Generation / Router instead. */ public static function getDomainStartPage($domain, $path = '') { + trigger_error('BackendUtility::getDomainStartPage() will be removed in TYPO3 v10.0. Use the new Link Generation functionality instead.', E_USER_DEPRECATED); $domain = explode(':', $domain); $domain = strtolower(preg_replace('/\\.$/', '', $domain[0])); // Path is calculated. diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst new file mode 100644 index 000000000000..30f539e0c592 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst @@ -0,0 +1,43 @@ +.. include:: ../../Includes.txt + +==================================================================== +Deprecation: #85892 - Various methods regarding sys_domain-resolving +==================================================================== + +See :issue:`85892` + +Description +=========== + +Various methods specific for handling `sys_domain` records have been deprecated. As the new site handling is in place in favor of using `sys_domain` +records, these methods have been centralized in a :php:`LegacyDomainResolver` class, which is however marked as internal. + +Instead, generating URLs should be done via the new PageUriBuilder and Routing API (still in progress), which covers both the new +site handling and the specific sys_domain record. + +The following methods have been deprecated: +* :php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->domainNameMatchesCurrentRequest()` +* :php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getDomainDataForPid()` +* :php:`TYPO3\CMS\Backend\Utility\BackendUtility::getDomainStartPage()` +* :php:`TYPO3\CMS\Backend\Utility\BackendUtility::firstDomainRecord()` + + +Impact +====== + +Calling any of the methods will trigger a deprecation message. + + +Affected Installations +====================== + +Any installation with custom functionality regarding sys_domain handling where any of the methods above are used. + + +Migration +========= + +Migrate to either the new Routing API (finalized for 9 LTS) or implement the functionality in your own, or use the :php:`LegacyDomainResolver` class, +but since the concept of sys_domain handling will be removed in TYPO3 v10.0, consider use the Site handling functionality instead. + +.. index:: FullyScanned \ No newline at end of file diff --git a/typo3/sysext/frontend/Classes/Compatibility/LegacyDomainResolver.php b/typo3/sysext/frontend/Classes/Compatibility/LegacyDomainResolver.php new file mode 100644 index 000000000000..1fe891a9be9e --- /dev/null +++ b/typo3/sysext/frontend/Classes/Compatibility/LegacyDomainResolver.php @@ -0,0 +1,282 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Frontend\Compatibility; + +/* + * 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\ServerRequestInterface; +use Symfony\Component\Routing\Exception\NoConfigurationException; +use Symfony\Component\Routing\Matcher\UrlMatcher; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; +use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Exception\Page\RootLineException; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\SingletonInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\RootlineUtility; + +/** + * Resolves sys_domain entries when a Request object is given, + * or a pageId is given or a rootpage Id is given (= if there is a sys_domain record on that specific page). + * Always keeps the sorting in line. + * + * @todo: would be nice to flush caches if sys_domain has been touched in DataHandler + * @internal as this should ideally be wrapped inside the "main" site router in the future. + */ +class LegacyDomainResolver implements SingletonInterface +{ + /** + * Runtime cache of domains per processed page ids. + * + * @var array + */ + protected $domainDataCache = []; + + /** + * @var FrontendInterface + */ + protected $cache; + + /** + * Whether a sys_domain like example.com should also match for my.blog.example.com + * + * @var bool + */ + protected $recursiveDomainSearch; + + /** + * @var RouteCollection + */ + protected $routeCollection; + + /** + * all entries in sys_domain + * @var array + */ + protected $allDomainRecords; + + /** + * all entries in sys_domain grouped by page (pid) + * @var array + */ + protected $groupedDomainsPerPage; + + public function __construct() + { + $this->cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_core'); + $this->recursiveDomainSearch = (bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['recursiveDomainSearch']; + $this->routeCollection = new RouteCollection(); + $this->populate(); + } + + /** + * Builds up all domain records from DB and all routes + */ + protected function populate() + { + if ($data = $this->cache->get('legacy-domains')) { + // Due to the nature of PhpFrontend, the `<?php` and `#` wraps have to be removed + $data = preg_replace('/^<\?php\s*|\s*#$/', '', $data); + $data = unserialize($data, ['allowed_classes' => [Route::class, RouteCollection::class]]); + $this->routeCollection = $data['routeCollection']; + $this->allDomainRecords = $data['allDomainRecords']; + $this->groupedDomainsPerPage = $data['groupedDomainsPerPage']; + } else { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain'); + $queryBuilder->getRestrictions()->removeAll(); + $statement = $queryBuilder + ->select('*') + ->from('sys_domain') + ->orderBy('sorting', 'ASC') + ->execute(); + + while ($row = $statement->fetch()) { + $row['domainName'] = rtrim($row['domainName'], '/'); + $this->allDomainRecords[(int)$row['uid']] = $row; + $this->groupedDomainsPerPage[(int)$row['pid']][] = $row; + if (!$row['hidden']) { + if (strpos($row['domainName'], '/') === false) { + $path = ''; + list($host, $port) = explode(':', $row['domainName']); + } else { + $urlParts = parse_url($row['domainName']); + $path = trim($urlParts['path'], '/'); + $host = $urlParts['host']; + $port = (string)$urlParts['port']; + } + $route = new Route( + $path . '/{next}', + ['pageId' => $row['pid']], + array_filter(['next' => '.*', 'port' => $port]), + ['utf8' => true], + $host ?? '' + ); + $this->routeCollection->add('domain_' . $row['uid'], $route); + } + } + + $data = [ + 'routeCollection' => $this->routeCollection, + 'allDomainRecords' => $this->allDomainRecords, + 'groupedDomainsPerPage' => $this->groupedDomainsPerPage + ]; + $this->cache->set('legacy-domains', serialize($data), ['sys_domain'], 0); + } + } + + /** + * 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". + * + * @param ServerRequestInterface $request + * @return int page ID + */ + public function matchRequest(ServerRequestInterface $request): int + { + if (empty($this->allDomainRecords) || count($this->routeCollection) === 0) { + return 0; + } + $context = new RequestContext('/', $request->getMethod(), $request->getUri()->getHost()); + $matcher = new UrlMatcher($this->routeCollection, $context); + if ($this->recursiveDomainSearch) { + $pageUid = 0; + $host = explode('.', $request->getUri()->getHost()); + while (count($host)) { + $context->setHost(implode('.', $host)); + try { + $result = $matcher->match($request->getUri()->getPath()); + return (int)$result['pageId']; + } catch (NoConfigurationException | ResourceNotFoundException $e) { + array_shift($host); + } + } + return $pageUid; + } + try { + $result = $matcher->match($request->getUri()->getPath()); + return (int)$result['pageId']; + } catch (NoConfigurationException | ResourceNotFoundException $e) { + // No domain record found + } + return 0; + } + + /** + * Obtains a sys_domain record that fits for a given page ID by traversing the rootline up and finding + * a suitable page with sys_domain records. + * As all sys_domains have been fetched already, the internal grouped list of sys_domains can be used directly. + * + * Usually used in the Frontend to find out the domain of a page to link to. + * + * Includes a runtime cache if a frontend request links to the same page multiple times. + * + * @param int $pageId Target page id + * @param ServerRequestInterface|null $currentRequest if given, the domain record is marked with "isCurrentDomain" + * @return array|null the sys_domain record if found + */ + public function matchPageId(int $pageId, ServerRequestInterface $currentRequest = null): ?array + { + // Using array_key_exists() here, nice $result can be NULL + // (happens, if there's no domain records defined) + if (array_key_exists($pageId, $this->domainDataCache)) { + return $this->domainDataCache[$pageId]; + } + try { + $this->domainDataCache[$pageId] = $this->resolveDomainEntry( + $pageId, + $currentRequest + ); + } catch (RootLineException $e) { + $this->domainDataCache[$pageId] = null; + } + return $this->domainDataCache[$pageId]; + } + + /** + * Returns the full sys_domain record, based on a page record, which is assumed the "pid" of the sys_domain record. + * Since ordering is taken into account, this is the first sys_domain record on that page Id. + * + * @param int $pageId + * @return array|null + */ + public function matchRootPageId(int $pageId): ?array + { + return !empty($this->groupedDomainsPerPage[$pageId]) ? reset($this->groupedDomainsPerPage[$pageId]) : null; + } + + /** + * @param int $pageId + * @param ServerRequestInterface|null $currentRequest + * @return array|null + */ + protected function resolveDomainEntry(int $pageId, ?ServerRequestInterface $currentRequest): ?array + { + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get(); + // walk the rootline downwards from the target page + // to the root page, until a domain record is found + foreach ($rootLine as $pageInRootline) { + $pidInRootline = $pageInRootline['uid']; + if (empty($this->groupedDomainsPerPage[$pidInRootline])) { + continue; + } + + $domainEntriesOfPage = $this->groupedDomainsPerPage[$pidInRootline]; + foreach ($domainEntriesOfPage as $domainEntry) { + if ($domainEntry['hidden']) { + continue; + } + // When no currentRequest is given, let's take the first non-hidden sys_domain page + if ($currentRequest === null) { + return $domainEntry; + } + // Otherwise the check should match against the current domain (and set "isCurrentDomain") + // Current domain is "forced", however, otherwise the first one is fine + if ($this->domainNameMatchesCurrentRequest($domainEntry['domainName'], $currentRequest)) { + $result = $domainEntry; + $result['isCurrentDomain'] = true; + return $result; + } + } + } + return null; + } + + /** + * Whether the given domain name (potentially including a path segment) matches currently requested host or + * the host including the path segment + * + * @param string $domainName + * @param ServerRequestInterface|null $request + * @return bool + */ + protected function domainNameMatchesCurrentRequest($domainName, ServerRequestInterface $request): bool + { + /** @var NormalizedParams $normalizedParams */ + $normalizedParams = $request->getAttribute('normalizedParams'); + if (!($normalizedParams instanceof NormalizedParams)) { + return false; + } + $currentDomain = $normalizedParams->getHttpHost(); + // remove the script filename from the path segment. + $currentPathSegment = trim(preg_replace('|/[^/]*$|', '', $normalizedParams->getScriptName())); + return $currentDomain === $domainName || $currentDomain . $currentPathSegment === $domainName; + } +} diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index b67412837130..ced4e5a46db4 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -34,7 +34,6 @@ use TYPO3\CMS\Core\Context\WorkspaceAspect; use TYPO3\CMS\Core\Controller\ErrorPageController; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; @@ -64,6 +63,7 @@ use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Core\Utility\RootlineUtility; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; +use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Http\UrlHandlerInterface; use TYPO3\CMS\Frontend\Page\CacheHashCalculator; @@ -759,13 +759,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ protected $cacheHash; - /** - * Runtime cache of domains per processed page ids. - * - * @var array - */ - protected $domainDataCache = []; - /** * Content type HTTP header being sent in the request. * @todo Ticket: #63642 Should be refactored to a request/response model later @@ -4710,57 +4703,17 @@ class TypoScriptFrontendController implements LoggerAwareInterface return $result; } - /** - * Fetches/returns the cached contents of the sys_domain database table. - * - * @return array Domain data - */ - protected function getSysDomainCache() - { - $entryIdentifier = 'core-database-sys_domain-complete'; - /** @var $runtimeCache \TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend */ - $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime'); - - $sysDomainData = []; - if ($runtimeCache->has($entryIdentifier)) { - $sysDomainData = $runtimeCache->get($entryIdentifier); - } else { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain'); - $queryBuilder->setRestrictions(GeneralUtility::makeInstance(DefaultRestrictionContainer::class)); - $result = $queryBuilder - ->select('uid', 'pid', 'domainName') - ->from('sys_domain') - ->orderBy('sorting', 'ASC') - ->execute(); - - while ($row = $result->fetch()) { - // If there is already an entry for this pid, we should not override it - // Except if it is the current domain - if (isset($sysDomainData[$row['pid']]) && !$this->domainNameMatchesCurrentRequest($row['domainName'])) { - continue; - } - - // as we passed all previous checks, we save this domain for the current pid - $sysDomainData[$row['pid']] = [ - 'uid' => $row['uid'], - 'pid' => $row['pid'], - 'domainName' => rtrim($row['domainName'], '/'), - ]; - } - $runtimeCache->set($entryIdentifier, $sysDomainData); - } - return $sysDomainData; - } - /** * Whether the given domain name (potentially including a path segment) matches currently requested host or * the host including the path segment * * @param string $domainName * @return bool + * @deprecated will be removed in TYPO3 v10. */ public function domainNameMatchesCurrentRequest($domainName) { + trigger_error('This method will be removed in TYPO3 v10, use LegacyDomainResolver instead.', E_USER_DEPRECATED); $currentDomain = GeneralUtility::getIndpEnv('HTTP_HOST'); $currentPathSegment = trim(preg_replace('|/[^/]*$|', '', GeneralUtility::getIndpEnv('SCRIPT_NAME'))); return $currentDomain === $domainName || $currentDomain . $currentPathSegment === $domainName; @@ -4772,32 +4725,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface * * @param int $targetPid Target page id * @return mixed Return domain data or NULL + * @deprecated will be removed in TYPO3 v10. */ public function getDomainDataForPid($targetPid) { - // Using array_key_exists() here, nice $result can be NULL - // (happens, if there's no domain records defined) - if (!array_key_exists($targetPid, $this->domainDataCache)) { - $result = null; - $sysDomainData = $this->getSysDomainCache(); - try { - $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $targetPid, null, $this->context)->get(); - // walk the rootline downwards from the target page - // to the root page, until a domain record is found - foreach ($rootLine as $pageInRootline) { - $pidInRootline = $pageInRootline['uid']; - if (isset($sysDomainData[$pidInRootline])) { - $result = $sysDomainData[$pidInRootline]; - break; - } - } - } catch (RootLineException $e) { - // Do nothing - } - $this->domainDataCache[$targetPid] = $result; - } - - return $this->domainDataCache[$targetPid]; + trigger_error('This method will be removed in TYPO3 v10, use LegacyDomainResolver instead.', E_USER_DEPRECATED); + return GeneralUtility::makeInstance(LegacyDomainResolver::class)->matchPageId((int)$targetPid, $GLOBALS['TYPO3_REQUEST']); } /** @@ -4806,12 +4739,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface * * @param int $targetPid Target page id * @return mixed Return domain name or NULL if not found - * @deprecated will be removed in TYPO3 v10, as getDomainDataForPid could work + * @deprecated will be removed in TYPO3 v10. */ public function getDomainNameForPid($targetPid) { - trigger_error('This method will be removed in TYPO3 v10, use $TSFE->getDomainDataForPid() instead.', E_USER_DEPRECATED); - $domainData = $this->getDomainDataForPid($targetPid); + trigger_error('This method will be removed in TYPO3 v10, use LegacyDomainResolver instead.', E_USER_DEPRECATED); + $domainData = GeneralUtility::makeInstance(LegacyDomainResolver::class)->matchPageId((int)$targetPid, $GLOBALS['TYPO3_REQUEST']); return $domainData ? $domainData['domainName'] : null; } diff --git a/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php b/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php index 9002c0cbc4dd..19f5bff9677e 100644 --- a/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php +++ b/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php @@ -23,13 +23,12 @@ use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Exception\SiteNotFoundException; -use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; @@ -145,83 +144,13 @@ class SiteResolver implements MiddlewareInterface if ($site instanceof Site) { $GLOBALS['TSFE']->domainStartPage = $site->getRootPageId(); } else { - $GLOBALS['TSFE']->domainStartPage = $this->findDomainRecord($request->getAttribute('normalizedParams'), (bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['recursiveDomainSearch']); + $GLOBALS['TSFE']->domainStartPage = GeneralUtility::makeInstance(LegacyDomainResolver::class) + ->matchRequest($request); } return $handler->handle($request); } - /** - * Looking up a domain record based on server parameters HTTP_HOST - * - * @param NormalizedParams $requestParams used to get sanitized information of the current request - * @param bool $recursive If set, it looks "recursively" meaning that a domain like "123.456.typo3.com" would find a domain record like "typo3.com" if "123.456.typo3.com" or "456.typo3.com" did not exist. - * @return int|null Returns the page id of the page where the domain record was found or null if no sys_domain record found. - * previously found at \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::findDomainRecord() - */ - protected function findDomainRecord(NormalizedParams $requestParams, $recursive = false): ?int - { - if ($recursive) { - $pageUid = 0; - $host = explode('.', $requestParams->getHttpHost()); - while (count($host)) { - $pageUid = $this->getRootPageIdFromDomainRecord(implode('.', $host), $requestParams->getScriptName()); - if ($pageUid) { - return $pageUid; - } - array_shift($host); - } - return $pageUid; - } - return $this->getRootPageIdFromDomainRecord($requestParams->getHttpHost(), $requestParams->getScriptName()); - } - - /** - * Will find the page ID carrying the domain record matching the input domain. - * - * @param string $domain Domain name to search for. Eg. "www.typo3.com". Typical the HTTP_HOST value. - * @param string $path Path for the current script in domain. Eg. "/somedir/subdir". Typ. supplied by \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('SCRIPT_NAME') - * @return int|null If found, returns integer with page UID where found. Otherwise null. - * previously found at PageRepository::getDomainStartPage - */ - protected function getRootPageIdFromDomainRecord(string $domain, string $path = ''): ?int - { - list($domain) = explode(':', $domain); - $domain = strtolower(preg_replace('/\\.$/', '', $domain)); - // Removing extra trailing slashes - $path = trim(preg_replace('/\\/[^\\/]*$/', '', $path)); - // Appending to domain string - $domain .= $path; - $domain = preg_replace('/\\/*$/', '', $domain); - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain'); - $queryBuilder->getRestrictions()->removeAll(); - $row = $queryBuilder - ->select( - 'pid' - ) - ->from('sys_domain') - ->where( - $queryBuilder->expr()->eq( - 'hidden', - $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->orX( - $queryBuilder->expr()->eq( - 'domainName', - $queryBuilder->createNamedParameter($domain, \PDO::PARAM_STR) - ), - $queryBuilder->expr()->eq( - 'domainName', - $queryBuilder->createNamedParameter($domain . '/', \PDO::PARAM_STR) - ) - ) - ) - ->setMaxResults(1) - ->execute() - ->fetch(); - return $row ? (int)$row['pid'] : null; - } - /** * Checks if the language is allowed in Frontend, if not, check if there is valid BE user * diff --git a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php index ae12ca0f75d0..30a2275ab0a2 100644 --- a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php +++ b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php @@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Core\Utility\RootlineUtility; +use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver; use TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface; use TYPO3\CMS\Frontend\Page\CacheHashCalculator; use TYPO3\CMS\Frontend\Page\PageRepository; @@ -154,12 +155,13 @@ class PageLinkBuilder extends AbstractTypolinkBuilder $page = $page2; } } - - $targetDomainRecord = $tsfe->getDomainDataForPid($page['uid']); - $targetDomain = $targetDomainRecord ? $targetDomainRecord['domainName'] : null; + // @todo: This obviously needs more detection with Site Handling, to detect the site language ID + $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class); + $targetDomainRecord = $domainResolver->matchPageId((int)$page['uid'], $GLOBALS['TYPO3_REQUEST']); + $targetDomain = ''; // Do not prepend the domain if it is the current hostname - if (!$targetDomain || $tsfe->domainNameMatchesCurrentRequest($targetDomain)) { - $targetDomain = ''; + if (!empty($targetDomainRecord) && !$targetDomainRecord['isCurrentDomain']) { + $targetDomain = $targetDomainRecord['domainName']; } } if ($conf['useCacheHash']) { diff --git a/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php index b6aec4eb432b..2570129de869 100644 --- a/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php +++ b/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php @@ -14,10 +14,6 @@ namespace TYPO3\CMS\Frontend\Tests\Functional\Controller; * The TYPO3 project - inspiring people to share! */ -use Doctrine\DBAL\Platforms\SQLServerPlatform; -use TYPO3\CMS\Core\Cache\CacheManager; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; @@ -85,62 +81,6 @@ class TypoScriptFrontendControllerTest extends FunctionalTestCase ); } - /** - * @param string $currentDomain - * @test - * @dataProvider getSysDomainCacheDataProvider - */ - public function getSysDomainCacheReturnsCurrentDomainRecord($currentDomain) - { - GeneralUtility::flushInternalRuntimeCaches(); - - $_SERVER['HTTP_HOST'] = $currentDomain; - $domainRecords = [ - 'typo3.org' => [ - 'uid' => '1', - 'pid' => '1', - 'domainName' => 'typo3.org', - ], - 'foo.bar' => [ - 'uid' => '2', - 'pid' => '1', - 'domainName' => 'foo.bar', - ], - 'example.com' => [ - 'uid' => '3', - 'pid' => '1', - 'domainName' => 'example.com', - ], - ]; - - $connection = (new ConnectionPool())->getConnectionForTable('sys_domain'); - - $sqlServerIdentityDisabled = false; - if ($connection->getDatabasePlatform() instanceof SQLServerPlatform) { - $connection->exec('SET IDENTITY_INSERT sys_domain ON'); - $sqlServerIdentityDisabled = true; - } - - foreach ($domainRecords as $domainRecord) { - $connection->insert( - 'sys_domain', - $domainRecord - ); - } - - if ($sqlServerIdentityDisabled) { - $connection->exec('SET IDENTITY_INSERT sys_domain OFF'); - } - - GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime')->flush(); - $expectedResult = [ - $domainRecords[$currentDomain]['pid'] => $domainRecords[$currentDomain], - ]; - - $actualResult = $this->tsFrontendController->_call('getSysDomainCache'); - $this->assertEquals($expectedResult, $actualResult); - } - /** * @param string $tablePid * @param int $now @@ -150,22 +90,4 @@ class TypoScriptFrontendControllerTest extends FunctionalTestCase { return $this->tsFrontendController->_call('getFirstTimeValueForRecord', $tablePid, $now); } - - /** - * @return array - */ - public function getSysDomainCacheDataProvider() - { - return [ - 'typo3.org' => [ - 'typo3.org', - ], - 'foo.bar' => [ - 'foo.bar', - ], - 'example.com' => [ - 'example.com', - ], - ]; - } } diff --git a/typo3/sysext/frontend/Tests/Unit/Compatibility/LegacyDomainResolverTest.php b/typo3/sysext/frontend/Tests/Unit/Compatibility/LegacyDomainResolverTest.php new file mode 100644 index 000000000000..11eaf0f496d7 --- /dev/null +++ b/typo3/sysext/frontend/Tests/Unit/Compatibility/LegacyDomainResolverTest.php @@ -0,0 +1,103 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\Tests\Unit\Compatibility; + +/* + * 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 TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\Http\ServerRequestFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class LegacyDomainResolverTest extends UnitTestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface|LegacyDomainResolver + */ + protected $subject; + + protected $resetSingletonInstances = true; + + protected $backupEnvironment = true; + + protected function setUp() + { + parent::setUp(); + $this->subject = $this->getAccessibleMock(LegacyDomainResolver::class, ['dummy'], [], '', false); + GeneralUtility::flushInternalRuntimeCaches(); + } + + /** + * Tests concerning domainNameMatchesCurrentRequest + */ + + /** + * @return array + */ + public function domainNameMatchesCurrentRequestDataProvider() + { + return [ + 'same domains' => [ + 'typo3.org', + 'typo3.org', + '/index.php', + true, + ], + 'same domains with subdomain' => [ + 'www.typo3.org', + 'www.typo3.org', + '/index.php', + true, + ], + 'different domains' => [ + 'foo.bar', + 'typo3.org', + '/index.php', + false, + ], + 'domain record with script name' => [ + 'typo3.org', + 'typo3.org/foo/bar', + '/foo/bar/index.php', + true, + ], + 'domain record with wrong script name' => [ + 'typo3.org', + 'typo3.org/foo/bar', + '/bar/foo/index.php', + false, + ], + ]; + } + + /** + * @param string $currentDomain + * @param string $domainRecord + * @param string $scriptName + * @param bool $expectedResult + * @test + * @dataProvider domainNameMatchesCurrentRequestDataProvider + */ + public function domainNameMatchesCurrentRequest(string $currentDomain, string $domainRecord, string $scriptName, bool $expectedResult) + { + $_SERVER['HTTP_HOST'] = $currentDomain; + $_SERVER['SCRIPT_NAME'] = $scriptName; + $request = ServerRequestFactory::fromGlobals(); + $normalizedParams = new NormalizedParams($request, [], Environment::getCurrentScript(), Environment::getPublicPath()); + $request = $request->withAttribute('normalizedParams', $normalizedParams); + $this->assertEquals($expectedResult, $this->subject->_call('domainNameMatchesCurrentRequest', $domainRecord, $request)); + } +} diff --git a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php index ef34c1c429ef..cace16836c21 100644 --- a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php @@ -144,64 +144,6 @@ class TypoScriptFrontendControllerTest extends UnitTestCase ]; } - /** - * Tests concerning domainNameMatchesCurrentRequest - */ - - /** - * @return array - */ - public function domainNameMatchesCurrentRequestDataProvider() - { - return [ - 'same domains' => [ - 'typo3.org', - 'typo3.org', - '/index.php', - true, - ], - 'same domains with subdomain' => [ - 'www.typo3.org', - 'www.typo3.org', - '/index.php', - true, - ], - 'different domains' => [ - 'foo.bar', - 'typo3.org', - '/index.php', - false, - ], - 'domain record with script name' => [ - 'typo3.org', - 'typo3.org/foo/bar', - '/foo/bar/index.php', - true, - ], - 'domain record with wrong script name' => [ - 'typo3.org', - 'typo3.org/foo/bar', - '/bar/foo/index.php', - false, - ], - ]; - } - - /** - * @param string $currentDomain - * @param string $domainRecord - * @param string $scriptName - * @param bool $expectedResult - * @test - * @dataProvider domainNameMatchesCurrentRequestDataProvider - */ - public function domainNameMatchesCurrentRequest($currentDomain, $domainRecord, $scriptName, $expectedResult) - { - $_SERVER['HTTP_HOST'] = $currentDomain; - $_SERVER['SCRIPT_NAME'] = $scriptName; - $this->assertEquals($expectedResult, $this->subject->domainNameMatchesCurrentRequest($domainRecord)); - } - /** * @return array */ diff --git a/typo3/sysext/frontend/Tests/UnitDeprecated/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/UnitDeprecated/Controller/TypoScriptFrontendControllerTest.php new file mode 100644 index 000000000000..758e0ea9f1e3 --- /dev/null +++ b/typo3/sysext/frontend/Tests/UnitDeprecated/Controller/TypoScriptFrontendControllerTest.php @@ -0,0 +1,96 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\Tests\UnitDeprecated\Controller; + +/* + * 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 TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * Test case + */ +class TypoScriptFrontendControllerTest extends UnitTestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface|TypoScriptFrontendController + */ + protected $subject; + + protected function setUp() + { + parent::setUp(); + GeneralUtility::flushInternalRuntimeCaches(); + $this->subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false); + } + + /** + * Tests concerning domainNameMatchesCurrentRequest + */ + + /** + * @return array + */ + public function domainNameMatchesCurrentRequestDataProvider() + { + return [ + 'same domains' => [ + 'typo3.org', + 'typo3.org', + '/index.php', + true, + ], + 'same domains with subdomain' => [ + 'www.typo3.org', + 'www.typo3.org', + '/index.php', + true, + ], + 'different domains' => [ + 'foo.bar', + 'typo3.org', + '/index.php', + false, + ], + 'domain record with script name' => [ + 'typo3.org', + 'typo3.org/foo/bar', + '/foo/bar/index.php', + true, + ], + 'domain record with wrong script name' => [ + 'typo3.org', + 'typo3.org/foo/bar', + '/bar/foo/index.php', + false, + ], + ]; + } + + /** + * @param string $currentDomain + * @param string $domainRecord + * @param string $scriptName + * @param bool $expectedResult + * @test + * @dataProvider domainNameMatchesCurrentRequestDataProvider + */ + public function domainNameMatchesCurrentRequest($currentDomain, $domainRecord, $scriptName, $expectedResult) + { + $_SERVER['HTTP_HOST'] = $currentDomain; + $_SERVER['SCRIPT_NAME'] = $scriptName; + $this->assertEquals($expectedResult, $this->subject->domainNameMatchesCurrentRequest($domainRecord)); + } +} diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php index 325a225600ca..52e6366ce844 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php @@ -2928,4 +2928,18 @@ return [ 'Deprecation-85878-EidUtilityAndVariousTSFEMethods.rst', ], ], + 'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->domainNameMatchesCurrentRequest' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 1, + 'restFiles' => [ + 'Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst', + ], + ], + 'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getDomainDataForPid' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 1, + 'restFiles' => [ + 'Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst', + ], + ], ]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php index 6bb0b0ad9345..b06338b3afab 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php @@ -771,4 +771,18 @@ return [ 'Deprecation-85878-EidUtilityAndVariousTSFEMethods.rst', ], ], + 'TYPO3\CMS\Backend\Utility\BackendUtility::getDomainStartPage' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 2, + 'restFiles' => [ + 'Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst', + ], + ], + 'TYPO3\CMS\Backend\Utility\BackendUtility::firstDomainRecord' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 1, + 'restFiles' => [ + 'Deprecation-85892-VariousMethodsRegardingSysDomainResolving.rst', + ], + ], ]; -- GitLab