[BUGFIX] Use sys_language isocode for SiteLanguage
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Site / PseudoSiteFinder.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Site;
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 TYPO3\CMS\Core\Cache\CacheManager;
20 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
23 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction;
24 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
25 use TYPO3\CMS\Core\Exception\Page\PageNotFoundException;
26 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
27 use TYPO3\CMS\Core\Site\Entity\NullSite;
28 use TYPO3\CMS\Core\Site\Entity\PseudoSite;
29 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\RootlineUtility;
32 use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
33
34 /**
35 * Methods related to "pseudo-sites" = sites that do not have a configuration yet.
36 * @internal this class will likely be removed in TYPO3 v10.0. Please use SiteMatcher and not the PseudoSiteFinder directly to make use of caching etc.
37 */
38 class PseudoSiteFinder
39 {
40 /**
41 * @var string
42 */
43 protected $cacheIdentifier = 'pseudo-sites';
44
45 /**
46 * @var FrontendInterface
47 */
48 protected $cache;
49
50 /**
51 * @var PseudoSite[]
52 */
53 protected $pseudoSites = [];
54
55 public function __construct()
56 {
57 $this->cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_core');
58 }
59
60 /**
61 * Fetches all site root pages, all sys_language and sys_domain records and forms pseudo-sites,
62 * but only for the pagetree's that do not have a site configuration available.
63 */
64 protected function populate()
65 {
66 $data = $this->cache->get($this->cacheIdentifier);
67 if (empty($data)) {
68 $allLanguages = $this->getAllLanguageRecords();
69 $groupedDomains = GeneralUtility::makeInstance(LegacyDomainResolver::class)->getGroupedDomainsPerPage();
70 $availablePages = $this->getAllRootPagesWithoutSiteConfiguration();
71 $this->cache->set($this->cacheIdentifier, json_encode([$allLanguages, $groupedDomains, $availablePages]));
72 } else {
73 // Due to the nature of PhpFrontend, the `<?php` and `#` wraps have to be removed
74 $data = preg_replace('/^<\?php\s*|\s*#$/', '', $data);
75 list($allLanguages, $groupedDomains, $availablePages) = json_decode($data, true);
76 }
77
78 $this->pseudoSites = [];
79 foreach ($availablePages as $row) {
80 $rootPageId = (int)$row['uid'];
81 $site = new PseudoSite($rootPageId, [
82 'domains' => $groupedDomains[$rootPageId] ?? [],
83 'languages' => $allLanguages
84 ]);
85 unset($groupedDomains[$rootPageId]);
86 $this->pseudoSites[$rootPageId] = $site;
87 }
88
89 // Now add the records where there is a sys_domain record but not configured as root page
90 foreach ($groupedDomains as $rootPageId => $domainRecords) {
91 $site = new PseudoSite((int)$rootPageId, [
92 'domains' => $domainRecords,
93 'languages' => $allLanguages
94 ]);
95 $this->pseudoSites[(int)$rootPageId] = $site;
96 }
97
98 // Now lets an empty Pseudo-Site for visiting things on pid=0
99 $this->pseudoSites[0] = new NullSite($allLanguages);
100 }
101
102 /**
103 * Returns all pseudo sites, including one for "pid=0".
104 *
105 * @return PseudoSite[]
106 */
107 public function findAll(): array
108 {
109 if (empty($this->pseudoSites)) {
110 $this->populate();
111 }
112 return $this->pseudoSites;
113 }
114
115 /**
116 * Fetches all "sys_language" records.
117 *
118 * @return array
119 */
120 protected function getAllLanguageRecords(): array
121 {
122 $languageRecords = [
123 0 => [
124 'languageId' => 0,
125 'title' => 'Default',
126 'flag' => '',
127 ],
128 ];
129
130 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_language');
131 $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
132 $statement = $queryBuilder
133 ->select('*')
134 ->from('sys_language')
135 ->orderBy('sorting')
136 ->execute();
137 while ($row = $statement->fetch()) {
138 $uid = (int)$row['uid'];
139 $languageRecords[$uid] = [
140 'languageId' => $uid,
141 'title' => $row['title'],
142 'iso-639-1' => $row['language_isocode'] ?? '',
143 'flag' => 'flags-' . $row['flag'],
144 'enabled' => !$row['hidden'],
145 ];
146 }
147
148 return $languageRecords;
149 }
150
151 /**
152 * Traverses the rootline of a page up until a PseudoSite was found.
153 * The main use-case here is in the TYPO3 Backend when the middleware tries to detect
154 * a PseudoSite
155 *
156 * @param int $pageId
157 * @param array $rootLine
158 * @return SiteInterface
159 * @throws SiteNotFoundException
160 */
161 public function getSiteByPageId(int $pageId, array $rootLine = null): SiteInterface
162 {
163 $this->findAll();
164 if (isset($this->pseudoSites[$pageId])) {
165 return $this->pseudoSites[$pageId];
166 }
167 if (!is_array($rootLine)) {
168 try {
169 $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
170 } catch (PageNotFoundException $e) {
171 $rootLine = [];
172 }
173 }
174 foreach ($rootLine as $pageInRootLine) {
175 if (isset($this->pseudoSites[(int)$pageInRootLine['uid']])) {
176 return $this->pseudoSites[(int)$pageInRootLine['uid']];
177 }
178 }
179 throw new SiteNotFoundException('No pseudo-site found in root line of page ' . $pageId, 1534710048);
180 }
181
182 /**
183 * Find a site by given root page id
184 *
185 * @param int $rootPageId the page ID (default language)
186 * @return SiteInterface
187 * @throws SiteNotFoundException
188 */
189 public function getSiteByRootPageId(int $rootPageId): SiteInterface
190 {
191 if (empty($this->pseudoSites)) {
192 $this->populate();
193 }
194 if (isset($this->pseudoSites[$rootPageId])) {
195 return $this->pseudoSites[$rootPageId];
196 }
197 throw new SiteNotFoundException('No pseudo-site found for root page id ' . $rootPageId, 1521668982);
198 }
199
200 /**
201 * Loads all sites with a configuration, and takes their rootPageId.
202 *
203 * @return array
204 */
205 protected function getExistingSiteConfigurationRootPageIds(): array
206 {
207 $usedPageIds = [];
208 $finder = GeneralUtility::makeInstance(SiteFinder::class);
209 $sites = $finder->getAllSites();
210 foreach ($sites as $site) {
211 $usedPageIds[] = $site->getRootPageId();
212 }
213 return $usedPageIds;
214 }
215
216 /**
217 * Do a SQL query for root pages (pid=0 or is_siteroot=1) that do not have a site configuration
218 * @return array
219 */
220 protected function getAllRootPagesWithoutSiteConfiguration(): array
221 {
222 $usedPageIds = $this->getExistingSiteConfigurationRootPageIds();
223 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
224 $queryBuilder->getRestrictions()->removeAll()
225 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
226 ->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class, 0, false));
227 $queryBuilder
228 ->select('uid')
229 ->from('pages')
230 ->where(
231 $queryBuilder->expr()->eq('sys_language_uid', 0),
232 $queryBuilder->expr()->orX(
233 $queryBuilder->expr()->eq('pid', 0),
234 $queryBuilder->expr()->eq('is_siteroot', 1)
235 )
236 )
237 ->orderBy('pid')
238 ->addOrderBy('sorting');
239
240 if (!empty($usedPageIds)) {
241 $queryBuilder->andWhere($queryBuilder->expr()->notIn('uid', $usedPageIds));
242 }
243 $availablePages = $queryBuilder->execute()->fetchAll();
244 return is_array($availablePages) ? $availablePages : [];
245 }
246 }