[FEATURE] Allow multiple domain variants per site
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Site / Entity / Site.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Site\Entity;
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\UriInterface;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Error\PageErrorHandler\FluidPageErrorHandler;
22 use TYPO3\CMS\Core\Error\PageErrorHandler\InvalidPageErrorHandlerException;
23 use TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler;
24 use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
25 use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerNotConfiguredException;
26 use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
27 use TYPO3\CMS\Core\Http\Uri;
28 use TYPO3\CMS\Core\Localization\LanguageService;
29 use TYPO3\CMS\Core\Routing\PageRouter;
30 use TYPO3\CMS\Core\Routing\RouterInterface;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33 /**
34 * Entity representing a single site with available languages
35 */
36 class Site implements SiteInterface
37 {
38 protected const ERRORHANDLER_TYPE_PAGE = 'Page';
39 protected const ERRORHANDLER_TYPE_FLUID = 'Fluid';
40 protected const ERRORHANDLER_TYPE_PHP = 'PHP';
41
42 /**
43 * @var string
44 */
45 protected $identifier;
46
47 /**
48 * @var UriInterface
49 */
50 protected $base;
51
52 /**
53 * @var int
54 */
55 protected $rootPageId;
56
57 /**
58 * Any attributes for this site
59 * @var array
60 */
61 protected $configuration;
62
63 /**
64 * @var SiteLanguage[]
65 */
66 protected $languages;
67
68 /**
69 * @var array
70 */
71 protected $errorHandlers;
72
73 /**
74 * Sets up a site object, and its languages and error handlers
75 *
76 * @param string $identifier
77 * @param int $rootPageId
78 * @param array $configuration
79 */
80 public function __construct(string $identifier, int $rootPageId, array $configuration)
81 {
82 $this->identifier = $identifier;
83 $this->rootPageId = $rootPageId;
84 $this->configuration = $configuration;
85 $configuration['languages'] = $configuration['languages'] ?: [
86 0 => [
87 'languageId' => 0,
88 'title' => 'Default',
89 'navigationTitle' => '',
90 'typo3Language' => 'default',
91 'flag' => 'us',
92 'locale' => 'en_US.UTF-8',
93 'iso-639-1' => 'en',
94 'hreflang' => 'en-US',
95 'direction' => '',
96 ]
97 ];
98 $baseUrl = $configuration['base'] ?? '';
99 if (isset($configuration['baseVariants']) && is_array($configuration['baseVariants'])) {
100 $expressionLanguageResolver = GeneralUtility::makeInstance(
101 Resolver::class,
102 'site',
103 []
104 );
105 foreach ($configuration['baseVariants'] as $baseVariant) {
106 $result = $expressionLanguageResolver->evaluate($baseVariant['condition']);
107 if ($result) {
108 $baseUrl = $baseVariant['base'];
109 break;
110 }
111 }
112 }
113 $this->base = new Uri($this->sanitizeBaseUrl($baseUrl));
114
115 foreach ($configuration['languages'] as $languageConfiguration) {
116 $languageUid = (int)$languageConfiguration['languageId'];
117 // site language has defined its own base, this is the case most of the time.
118 if (!empty($languageConfiguration['base'])) {
119 $base = new Uri($this->sanitizeBaseUrl($languageConfiguration['base']));
120 // no host given by the language-specific base, so lets prefix the main site base
121 if ($base->getScheme() === null && $base->getHost() === '') {
122 $base = rtrim((string)$this->base, '/') . '/' . ltrim((string)$base, '/');
123 $base = new Uri($this->sanitizeBaseUrl($base));
124 }
125 } else {
126 // Language configuration does not have a base defined
127 // So the main site base is used (usually done for default languages)
128 $base = new Uri($this->sanitizeBaseUrl(rtrim((string)$this->base, '/') . '/'));
129 }
130 if (!empty($languageConfiguration['flag'])) {
131 if ($languageConfiguration['flag'] === 'global') {
132 $languageConfiguration['flag'] = 'flags-multiple';
133 } elseif ($languageConfiguration['flag'] !== 'empty-empty') {
134 $languageConfiguration['flag'] = 'flags-' . $languageConfiguration['flag'];
135 }
136 }
137 $this->languages[$languageUid] = new SiteLanguage(
138 $languageUid,
139 $languageConfiguration['locale'],
140 $base,
141 $languageConfiguration
142 );
143 }
144 foreach ($configuration['errorHandling'] ?? [] as $errorHandlingConfiguration) {
145 $code = $errorHandlingConfiguration['errorCode'];
146 unset($errorHandlingConfiguration['errorCode']);
147 $this->errorHandlers[(int)$code] = $errorHandlingConfiguration;
148 }
149 }
150
151 /**
152 * Gets the identifier of this site,
153 * mainly used when maintaining / configuring sites.
154 *
155 * @return string
156 */
157 public function getIdentifier(): string
158 {
159 return $this->identifier;
160 }
161
162 /**
163 * Returns the base URL of this site
164 *
165 * @return UriInterface
166 */
167 public function getBase(): UriInterface
168 {
169 return $this->base;
170 }
171
172 /**
173 * Returns the root page ID of this site
174 *
175 * @return int
176 */
177 public function getRootPageId(): int
178 {
179 return $this->rootPageId;
180 }
181
182 /**
183 * Returns all available languages of this site
184 *
185 * @return SiteLanguage[]
186 */
187 public function getLanguages(): array
188 {
189 $languages = [];
190 foreach ($this->languages as $languageId => $language) {
191 if ($language->enabled()) {
192 $languages[$languageId] = $language;
193 }
194 }
195 return $languages;
196 }
197
198 /**
199 * Returns all available languages of this site, even the ones disabled for frontend usages
200 *
201 * @return SiteLanguage[]
202 */
203 public function getAllLanguages(): array
204 {
205 return $this->languages;
206 }
207
208 /**
209 * Returns a language of this site, given by the sys_language_uid
210 *
211 * @param int $languageId
212 * @return SiteLanguage
213 * @throws \InvalidArgumentException
214 */
215 public function getLanguageById(int $languageId): SiteLanguage
216 {
217 if (isset($this->languages[$languageId])) {
218 return $this->languages[$languageId];
219 }
220 throw new \InvalidArgumentException(
221 'Language ' . $languageId . ' does not exist on site ' . $this->identifier . '.',
222 1522960188
223 );
224 }
225
226 /**
227 * @inheritdoc
228 */
229 public function getDefaultLanguage(): SiteLanguage
230 {
231 return reset($this->languages);
232 }
233
234 /**
235 * @inheritdoc
236 */
237 public function getAvailableLanguages(BackendUserAuthentication $user, bool $includeAllLanguagesFlag = false, int $pageId = null): array
238 {
239 $availableLanguages = [];
240
241 // Check if we need to add language "-1"
242 if ($includeAllLanguagesFlag && $user->checkLanguageAccess(-1)) {
243 $availableLanguages[-1] = new SiteLanguage(-1, '', $this->getBase(), [
244 'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:multipleLanguages'),
245 'flag' => 'flag-multiple'
246 ]);
247 }
248
249 // Do not add the ones that are not allowed by the user
250 foreach ($this->languages as $language) {
251 if ($user->checkLanguageAccess($language->getLanguageId())) {
252 $availableLanguages[$language->getLanguageId()] = $language;
253 }
254 }
255
256 return $availableLanguages;
257 }
258
259 /**
260 * Returns a ready-to-use error handler, to be used within the ErrorController
261 *
262 * @param int $statusCode
263 * @return PageErrorHandlerInterface
264 * @throws PageErrorHandlerNotConfiguredException
265 * @throws InvalidPageErrorHandlerException
266 */
267 public function getErrorHandler(int $statusCode): PageErrorHandlerInterface
268 {
269 $errorHandlerConfiguration = $this->errorHandlers[$statusCode] ?? null;
270 switch ($errorHandlerConfiguration['errorHandler']) {
271 case self::ERRORHANDLER_TYPE_FLUID:
272 return GeneralUtility::makeInstance(FluidPageErrorHandler::class, $statusCode, $errorHandlerConfiguration);
273 case self::ERRORHANDLER_TYPE_PAGE:
274 return GeneralUtility::makeInstance(PageContentErrorHandler::class, $statusCode, $errorHandlerConfiguration);
275 case self::ERRORHANDLER_TYPE_PHP:
276 $handler = GeneralUtility::makeInstance($errorHandlerConfiguration['errorPhpClassFQCN'], $statusCode, $errorHandlerConfiguration);
277 // Check if the interface is implemented
278 if (!($handler instanceof PageErrorHandlerInterface)) {
279 throw new InvalidPageErrorHandlerException('The configured error handler "' . (string)$errorHandlerConfiguration['errorPhpClassFQCN'] . '" for status code ' . $statusCode . ' must implement the PageErrorHandlerInterface.', 1527432330);
280 }
281 return $handler;
282 }
283 throw new PageErrorHandlerNotConfiguredException('No error handler given for the status code "' . $statusCode . '".', 1522495914);
284 }
285
286 /**
287 * Returns the whole configuration for this site
288 *
289 * @return array
290 */
291 public function getConfiguration(): array
292 {
293 return $this->configuration;
294 }
295
296 /**
297 * Returns a single configuration attribute
298 *
299 * @param string $attributeName
300 * @return mixed
301 * @throws \InvalidArgumentException
302 */
303 public function getAttribute(string $attributeName)
304 {
305 if (isset($this->configuration[$attributeName])) {
306 return $this->configuration[$attributeName];
307 }
308 throw new \InvalidArgumentException(
309 'Attribute ' . $attributeName . ' does not exist on site ' . $this->identifier . '.',
310 1522495954
311 );
312 }
313
314 /**
315 * If a site base contains "/" or "www.domain.com", it is ensured that
316 * parse_url() can handle this kind of configuration properly.
317 *
318 * @param string $base
319 * @return string
320 */
321 protected function sanitizeBaseUrl(string $base): string
322 {
323 // no protocol ("//") and the first part is no "/" (path), means that this is a domain like
324 // "www.domain.com/blabla", and we want to ensure that this one then gets a "no-scheme agnostic" part
325 if (!empty($base) && strpos($base, '//') === false && $base{0} !== '/') {
326 // either a scheme is added, or no scheme but with domain, or a path which is not absolute
327 // make the base prefixed with a slash, so it is recognized as path, not as domain
328 // treat as path
329 if (strpos($base, '.') === false) {
330 $base = '/' . $base;
331 } else {
332 // treat as domain name
333 $base = '//' . $base;
334 }
335 }
336 return $base;
337 }
338
339 /**
340 * Returns applicable routers for this site
341 *
342 * @return RouterInterface[]
343 */
344 public function getRouters(): array
345 {
346 return [
347 new PageRouter()
348 ];
349 }
350
351 /**
352 * Shorthand functionality for fetching the language service
353 * @return LanguageService
354 */
355 protected function getLanguageService(): LanguageService
356 {
357 return $GLOBALS['LANG'];
358 }
359 }