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