[FEATURE] Add "Pseudo" Site functionality
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Compatibility / LegacyDomainResolver.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Frontend\Compatibility;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Core\Cache\CacheManager;
21 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
22 use TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Exception\Page\RootLineException;
24 use TYPO3\CMS\Core\Http\NormalizedParams;
25 use TYPO3\CMS\Core\SingletonInterface;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Core\Utility\RootlineUtility;
28
29 /**
30 * Resolves sys_domain entries when a Request object is given,
31 * or a pageId is given or a rootpage Id is given (= if there is a sys_domain record on that specific page).
32 * Always keeps the sorting in line.
33 *
34 * @todo: would be nice to flush caches if sys_domain has been touched in DataHandler
35 * @internal as this should ideally be wrapped inside the "main" site router in the future.
36 */
37 class LegacyDomainResolver implements SingletonInterface
38 {
39 /**
40 * Runtime cache of domains per processed page ids.
41 *
42 * @var array
43 */
44 protected $domainDataCache = [];
45
46 /**
47 * @var FrontendInterface
48 */
49 protected $cache;
50
51 /**
52 * all entries in sys_domain grouped by page (pid)
53 * @var array
54 */
55 protected $groupedDomainsPerPage;
56
57 public function __construct()
58 {
59 $this->cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_core');
60 $this->populate();
61 }
62
63 /**
64 * Builds up all domain records from DB and all routes
65 */
66 protected function populate()
67 {
68 if ($data = $this->cache->get('legacy-domains')) {
69 // Due to the nature of PhpFrontend, the `<?php` and `#` wraps have to be removed
70 $data = preg_replace('/^<\?php\s*|\s*#$/', '', $data);
71 $this->groupedDomainsPerPage = json_decode($data, true);
72 } else {
73 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain');
74 $queryBuilder->getRestrictions()->removeAll();
75 $statement = $queryBuilder
76 ->select('*')
77 ->from('sys_domain')
78 ->orderBy('sorting', 'ASC')
79 ->execute();
80
81 while ($row = $statement->fetch()) {
82 $row['domainName'] = rtrim($row['domainName'], '/');
83 $this->groupedDomainsPerPage[(int)$row['pid']][] = $row;
84 }
85
86 $this->cache->set('legacy-domains', json_encode($this->groupedDomainsPerPage));
87 }
88 }
89
90 /**
91 * @return array
92 */
93 public function getGroupedDomainsPerPage(): array
94 {
95 return $this->groupedDomainsPerPage ?? [];
96 }
97
98 /**
99 * Obtains a sys_domain record that fits for a given page ID by traversing the rootline up and finding
100 * a suitable page with sys_domain records.
101 * As all sys_domains have been fetched already, the internal grouped list of sys_domains can be used directly.
102 *
103 * Usually used in the Frontend to find out the domain of a page to link to.
104 *
105 * Includes a runtime cache if a frontend request links to the same page multiple times.
106 *
107 * @param int $pageId Target page id
108 * @param ServerRequestInterface|null $currentRequest if given, the domain record is marked with "isCurrentDomain"
109 * @return array|null the sys_domain record if found
110 */
111 public function matchPageId(int $pageId, ServerRequestInterface $currentRequest = null): ?array
112 {
113 // Using array_key_exists() here, nice $result can be NULL
114 // (happens, if there's no domain records defined)
115 if (array_key_exists($pageId, $this->domainDataCache)) {
116 return $this->domainDataCache[$pageId];
117 }
118 try {
119 $this->domainDataCache[$pageId] = $this->resolveDomainEntry(
120 $pageId,
121 $currentRequest
122 );
123 } catch (RootLineException $e) {
124 $this->domainDataCache[$pageId] = null;
125 }
126 return $this->domainDataCache[$pageId];
127 }
128
129 /**
130 * Returns the full sys_domain record, based on a page record, which is assumed the "pid" of the sys_domain record.
131 * Since ordering is taken into account, this is the first sys_domain record on that page Id.
132 *
133 * @param int $pageId
134 * @return array|null
135 */
136 public function matchRootPageId(int $pageId): ?array
137 {
138 return !empty($this->groupedDomainsPerPage[$pageId]) ? reset($this->groupedDomainsPerPage[$pageId]) : null;
139 }
140
141 /**
142 * @param int $pageId
143 * @param ServerRequestInterface|null $currentRequest
144 * @return array|null
145 */
146 protected function resolveDomainEntry(int $pageId, ?ServerRequestInterface $currentRequest): ?array
147 {
148 $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
149 // walk the rootline downwards from the target page
150 // to the root page, until a domain record is found
151 foreach ($rootLine as $pageInRootline) {
152 $pidInRootline = $pageInRootline['uid'];
153 if (empty($this->groupedDomainsPerPage[$pidInRootline])) {
154 continue;
155 }
156
157 $domainEntriesOfPage = $this->groupedDomainsPerPage[$pidInRootline];
158 foreach ($domainEntriesOfPage as $domainEntry) {
159 if ($domainEntry['hidden']) {
160 continue;
161 }
162 // When no currentRequest is given, let's take the first non-hidden sys_domain page
163 if ($currentRequest === null) {
164 return $domainEntry;
165 }
166 // Otherwise the check should match against the current domain (and set "isCurrentDomain")
167 // Current domain is "forced", however, otherwise the first one is fine
168 if ($this->domainNameMatchesCurrentRequest($domainEntry['domainName'], $currentRequest)) {
169 $result = $domainEntry;
170 $result['isCurrentDomain'] = true;
171 return $result;
172 }
173 }
174 }
175 return null;
176 }
177
178 /**
179 * Whether the given domain name (potentially including a path segment) matches currently requested host or
180 * the host including the path segment
181 *
182 * @param string $domainName
183 * @param ServerRequestInterface|null $request
184 * @return bool
185 */
186 protected function domainNameMatchesCurrentRequest($domainName, ServerRequestInterface $request): bool
187 {
188 /** @var NormalizedParams $normalizedParams */
189 $normalizedParams = $request->getAttribute('normalizedParams');
190 if (!($normalizedParams instanceof NormalizedParams)) {
191 return false;
192 }
193 $currentDomain = $normalizedParams->getHttpHost();
194 // remove the script filename from the path segment.
195 $currentPathSegment = trim(preg_replace('|/[^/]*$|', '', $normalizedParams->getScriptName()));
196 return $currentDomain === $domainName || $currentDomain . $currentPathSegment === $domainName;
197 }
198 }