[BUGFIX] Streamline base variants for sites
[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'] = !empty($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 (is_array($configuration['baseVariants'] ?? false)) {
100 $expressionLanguageResolver = GeneralUtility::makeInstance(
101 Resolver::class,
102 'site',
103 []
104 );
105 foreach ($configuration['baseVariants'] as $baseVariant) {
106 if ($expressionLanguageResolver->evaluate($baseVariant['condition'])) {
107 $baseUrl = $baseVariant['base'];
108 break;
109 }
110 }
111 }
112 $this->base = new Uri($this->sanitizeBaseUrl($baseUrl));
113
114 foreach ($configuration['languages'] as $languageConfiguration) {
115 $languageUid = (int)$languageConfiguration['languageId'];
116 // site language has defined its own base, this is the case most of the time.
117 if (!empty($languageConfiguration['base'])) {
118 $base = $languageConfiguration['base'];
119 if (is_array($languageConfiguration['baseVariants'] ?? false)) {
120 $expressionLanguageResolver = $expressionLanguageResolver ?? GeneralUtility::makeInstance(
121 Resolver::class,
122 'site',
123 []
124 );
125 foreach ($languageConfiguration['baseVariants'] as $baseVariant) {
126 if ($expressionLanguageResolver->evaluate($baseVariant['condition'])) {
127 $base = $baseVariant['base'];
128 break;
129 }
130 }
131 }
132 $base = new Uri($this->sanitizeBaseUrl($base));
133 // no host given by the language-specific base, so lets prefix the main site base
134 if ($base->getScheme() === null && $base->getHost() === '') {
135 $base = rtrim((string)$this->base, '/') . '/' . ltrim((string)$base, '/');
136 $base = new Uri($this->sanitizeBaseUrl($base));
137 }
138 } else {
139 // Language configuration does not have a base defined
140 // So the main site base is used (usually done for default languages)
141 $base = new Uri($this->sanitizeBaseUrl(rtrim((string)$this->base, '/') . '/'));
142 }
143 if (!empty($languageConfiguration['flag'])) {
144 if ($languageConfiguration['flag'] === 'global') {
145 $languageConfiguration['flag'] = 'flags-multiple';
146 } elseif ($languageConfiguration['flag'] !== 'empty-empty') {
147 $languageConfiguration['flag'] = 'flags-' . $languageConfiguration['flag'];
148 }
149 }
150 $this->languages[$languageUid] = new SiteLanguage(
151 $languageUid,
152 $languageConfiguration['locale'],
153 $base,
154 $languageConfiguration
155 );
156 }
157 foreach ($configuration['errorHandling'] ?? [] as $errorHandlingConfiguration) {
158 $code = $errorHandlingConfiguration['errorCode'];
159 unset($errorHandlingConfiguration['errorCode']);
160 $this->errorHandlers[(int)$code] = $errorHandlingConfiguration;
161 }
162 }
163
164 /**
165 * Gets the identifier of this site,
166 * mainly used when maintaining / configuring sites.
167 *
168 * @return string
169 */
170 public function getIdentifier(): string
171 {
172 return $this->identifier;
173 }
174
175 /**
176 * Returns the base URL of this site
177 *
178 * @return UriInterface
179 */
180 public function getBase(): UriInterface
181 {
182 return $this->base;
183 }
184
185 /**
186 * Returns the root page ID of this site
187 *
188 * @return int
189 */
190 public function getRootPageId(): int
191 {
192 return $this->rootPageId;
193 }
194
195 /**
196 * Returns all available languages of this site
197 *
198 * @return SiteLanguage[]
199 */
200 public function getLanguages(): array
201 {
202 $languages = [];
203 foreach ($this->languages as $languageId => $language) {
204 if ($language->enabled()) {
205 $languages[$languageId] = $language;
206 }
207 }
208 return $languages;
209 }
210
211 /**
212 * Returns all available languages of this site, even the ones disabled for frontend usages
213 *
214 * @return SiteLanguage[]
215 */
216 public function getAllLanguages(): array
217 {
218 return $this->languages;
219 }
220
221 /**
222 * Returns a language of this site, given by the sys_language_uid
223 *
224 * @param int $languageId
225 * @return SiteLanguage
226 * @throws \InvalidArgumentException
227 */
228 public function getLanguageById(int $languageId): SiteLanguage
229 {
230 if (isset($this->languages[$languageId])) {
231 return $this->languages[$languageId];
232 }
233 throw new \InvalidArgumentException(
234 'Language ' . $languageId . ' does not exist on site ' . $this->identifier . '.',
235 1522960188
236 );
237 }
238
239 /**
240 * @inheritdoc
241 */
242 public function getDefaultLanguage(): SiteLanguage
243 {
244 return reset($this->languages);
245 }
246
247 /**
248 * @inheritdoc
249 */
250 public function getAvailableLanguages(BackendUserAuthentication $user, bool $includeAllLanguagesFlag = false, int $pageId = null): array
251 {
252 $availableLanguages = [];
253
254 // Check if we need to add language "-1"
255 if ($includeAllLanguagesFlag && $user->checkLanguageAccess(-1)) {
256 $availableLanguages[-1] = new SiteLanguage(-1, '', $this->getBase(), [
257 'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:multipleLanguages'),
258 'flag' => 'flag-multiple'
259 ]);
260 }
261
262 // Do not add the ones that are not allowed by the user
263 foreach ($this->languages as $language) {
264 if ($user->checkLanguageAccess($language->getLanguageId())) {
265 $availableLanguages[$language->getLanguageId()] = $language;
266 }
267 }
268
269 return $availableLanguages;
270 }
271
272 /**
273 * Returns a ready-to-use error handler, to be used within the ErrorController
274 *
275 * @param int $statusCode
276 * @return PageErrorHandlerInterface
277 * @throws PageErrorHandlerNotConfiguredException
278 * @throws InvalidPageErrorHandlerException
279 */
280 public function getErrorHandler(int $statusCode): PageErrorHandlerInterface
281 {
282 $errorHandlerConfiguration = $this->errorHandlers[$statusCode] ?? null;
283 switch ($errorHandlerConfiguration['errorHandler']) {
284 case self::ERRORHANDLER_TYPE_FLUID:
285 return GeneralUtility::makeInstance(FluidPageErrorHandler::class, $statusCode, $errorHandlerConfiguration);
286 case self::ERRORHANDLER_TYPE_PAGE:
287 return GeneralUtility::makeInstance(PageContentErrorHandler::class, $statusCode, $errorHandlerConfiguration);
288 case self::ERRORHANDLER_TYPE_PHP:
289 $handler = GeneralUtility::makeInstance($errorHandlerConfiguration['errorPhpClassFQCN'], $statusCode, $errorHandlerConfiguration);
290 // Check if the interface is implemented
291 if (!($handler instanceof PageErrorHandlerInterface)) {
292 throw new InvalidPageErrorHandlerException('The configured error handler "' . (string)$errorHandlerConfiguration['errorPhpClassFQCN'] . '" for status code ' . $statusCode . ' must implement the PageErrorHandlerInterface.', 1527432330);
293 }
294 return $handler;
295 }
296 throw new PageErrorHandlerNotConfiguredException('No error handler given for the status code "' . $statusCode . '".', 1522495914);
297 }
298
299 /**
300 * Returns the whole configuration for this site
301 *
302 * @return array
303 */
304 public function getConfiguration(): array
305 {
306 return $this->configuration;
307 }
308
309 /**
310 * Returns a single configuration attribute
311 *
312 * @param string $attributeName
313 * @return mixed
314 * @throws \InvalidArgumentException
315 */
316 public function getAttribute(string $attributeName)
317 {
318 if (isset($this->configuration[$attributeName])) {
319 return $this->configuration[$attributeName];
320 }
321 throw new \InvalidArgumentException(
322 'Attribute ' . $attributeName . ' does not exist on site ' . $this->identifier . '.',
323 1522495954
324 );
325 }
326
327 /**
328 * If a site base contains "/" or "www.domain.com", it is ensured that
329 * parse_url() can handle this kind of configuration properly.
330 *
331 * @param string $base
332 * @return string
333 */
334 protected function sanitizeBaseUrl(string $base): string
335 {
336 // no protocol ("//") and the first part is no "/" (path), means that this is a domain like
337 // "www.domain.com/blabla", and we want to ensure that this one then gets a "no-scheme agnostic" part
338 if (!empty($base) && strpos($base, '//') === false && $base{0} !== '/') {
339 // either a scheme is added, or no scheme but with domain, or a path which is not absolute
340 // make the base prefixed with a slash, so it is recognized as path, not as domain
341 // treat as path
342 if (strpos($base, '.') === false) {
343 $base = '/' . $base;
344 } else {
345 // treat as domain name
346 $base = '//' . $base;
347 }
348 }
349 return $base;
350 }
351
352 /**
353 * Returns the applicable router for this site. This might be configurable in the future.
354 *
355 * @return RouterInterface
356 */
357 public function getRouter(): RouterInterface
358 {
359 return GeneralUtility::makeInstance(PageRouter::class, $this);
360 }
361
362 /**
363 * Shorthand functionality for fetching the language service
364 * @return LanguageService
365 */
366 protected function getLanguageService(): LanguageService
367 {
368 return $GLOBALS['LANG'];
369 }
370 }