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