TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Entity;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Entity representing a site for everything on "pid=0". Mostly used in TYPO3 Backend, not really in use elsewhere.
*/
class NullSite implements SiteInterface
{
protected int $rootPageId = 0;
/**
* @var SiteLanguage[]
*/
protected array $languages;
/**
* Sets up a null site object
*
* @param array|null $languages site languages
* @param Uri|null $baseEntryPoint
*/
public function __construct(?array $languages = null, ?Uri $baseEntryPoint = null)
{
if (empty($languages)) {
// Create the default language if no language configuration is given
$this->languages[0] = new SiteLanguage(
0,
'',
new Uri('/'),
['enabled' => true]
);
} else {
foreach ($languages as $languageConfiguration) {
$languageUid = (int)$languageConfiguration['languageId'];
// Language configuration does not have a base defined
// So the main site base is used (usually done for default languages)
$this->languages[$languageUid] = new SiteLanguage(
$languageUid,
$languageConfiguration['locale'] ?? '',
$baseEntryPoint ?: new Uri('/'),
$languageConfiguration
);
}
}
}
/**
* Returns always #NULL
*/
public function getIdentifier(): string
{
return '#NULL';
}
/**
* Always "/"
*/
public function getBase(): UriInterface
{
return new Uri('/');
}
/**
* Always zero
*/
public function getRootPageId(): int
{
return 0;
}
/**
* Returns all available languages of this installation
*
* @return SiteLanguage[]
*/
public function getLanguages(): array
{
return $this->languages;
}
/**
* Returns a language of this site, given by the sys_language_uid
*
* @throws \InvalidArgumentException
*/
public function getLanguageById(int $languageId): SiteLanguage
{
if (isset($this->languages[$languageId])) {
return $this->languages[$languageId];
}
throw new \InvalidArgumentException(
'Language ' . $languageId . ' does not exist on site ' . $this->getIdentifier() . '.',
1522965188
);
}
public function getDefaultLanguage(): SiteLanguage
{
return reset($this->languages);
}
/**
* This takes page TSconfig into account (unlike Site interface) to find
* mod.SHARED.disableLanguages and mod.SHARED.defaultLanguageLabel
*/
public function getAvailableLanguages(BackendUserAuthentication $user, bool $includeAllLanguagesFlag = false, ?int $pageId = null): array
{
$availableLanguages = [];
// Check if we need to add language "-1"
if ($includeAllLanguagesFlag && $user->checkLanguageAccess(-1)) {
$availableLanguages[-1] = new SiteLanguage(-1, '', $this->getBase(), [
'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:multipleLanguages'),
'flag' => 'flags-multiple',
]);
}
$pageTs = BackendUtility::getPagesTSconfig((int)$pageId);
$pageTs = $pageTs['mod.']['SHARED.'] ?? [];
$disabledLanguages = GeneralUtility::intExplode(',', (string)($pageTs['disableLanguages'] ?? ''), true);
// Do not add the ones that are not allowed by the user
foreach ($this->languages as $language) {
if ($user->checkLanguageAccess($language) && !in_array($language->getLanguageId(), $disabledLanguages, true)) {
if ($language->getLanguageId() === 0) {
// 0: "Default" language
$defaultLanguageLabel = 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage';
$defaultLanguageLabel = $this->getLanguageService()->sL($defaultLanguageLabel);
if (isset($pageTs['defaultLanguageLabel'])) {
$defaultLanguageLabel = $pageTs['defaultLanguageLabel'] . ' (' . $defaultLanguageLabel . ')';
}
$defaultLanguageFlag = '';
if (isset($pageTs['defaultLanguageFlag'])) {
$defaultLanguageFlag = 'flags-' . $pageTs['defaultLanguageFlag'];
}
$language = new SiteLanguage(0, '', $language->getBase(), [
'title' => $defaultLanguageLabel,
'flag' => $defaultLanguageFlag,
]);
}
$availableLanguages[$language->getLanguageId()] = $language;
}
}
return $availableLanguages;
}
/**
* Returns a ready-to-use error handler, to be used within the ErrorController
*/
public function getErrorHandler(int $statusCode): PageErrorHandlerInterface
{
throw new \RuntimeException('No error handler given for the status code "' . $statusCode . '".', 1522495102);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+435
View File
@@ -0,0 +1,435 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Entity;
use Psr\Http\Message\UriInterface;
use Symfony\Component\ExpressionLanguage\SyntaxError;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Error\PageErrorHandler\FluidPageErrorHandler;
use TYPO3\CMS\Core\Error\PageErrorHandler\InvalidPageErrorHandlerException;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerNotConfiguredException;
use TYPO3\CMS\Core\Error\PageErrorHandler\RedirectLoginErrorHandler;
use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Routing\PageRouter;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Site\Set\SetError;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Entity representing a single site with available languages
*
* @phpstan-type LanguageRef -1|0|positive-int
*/
class Site implements SiteInterface
{
protected const ERRORHANDLER_TYPE_PAGE = 'Page';
protected const ERRORHANDLER_TYPE_FLUID = 'Fluid';
protected const ERRORHANDLER_TYPE_PHP = 'PHP';
protected const ERRORHANDLER_TYPE_LOGIN_REDIRECT = 'LoginRedirect';
/**
* @var string
*/
protected $identifier;
/**
* @var UriInterface
*/
protected $base;
/**
* @var int
*/
protected $rootPageId;
/**
* Any attributes for this site
* @var array
*/
protected $configuration;
/**
* Raw attributes for this site
* @var array
*/
protected $rawConfiguration;
/**
* @var array<LanguageRef, SiteLanguage>
*/
protected $languages;
/**
* @var list<string>
*/
protected array $sets;
/**
* @var array<string, array{error: SetError, name: string, context: string}>
*/
public array $invalidSets = [];
/**
* @var array
*/
protected $errorHandlers;
protected SiteSettings $settings;
protected ?SiteTypoScript $typoscript;
protected ?SiteTSconfig $tsConfig;
/**
* Sets up a site object, and its languages, error handlers and the settings
*/
public function __construct(string $identifier, int $rootPageId, array $configuration, ?SiteSettings $settings = null, ?SiteTypoScript $typoscript = null, ?SiteTSconfig $tsConfig = null)
{
$this->identifier = $identifier;
$this->rootPageId = $rootPageId;
if ($settings === null) {
// @todo deprecate null settings argument
$settings = SiteSettings::createFromSettingsTree($configuration['settings'] ?? []);
}
$this->settings = $settings;
$this->typoscript = $typoscript;
$this->tsConfig = $tsConfig;
$this->rawConfiguration = $configuration;
// Merge settings back in configuration for backwards-compatibility
$configuration['settings'] = $this->settings->getAll();
$this->configuration = $configuration;
$configuration['languages'] = !empty($configuration['languages']) ? $configuration['languages'] : [
0 => [
'languageId' => 0,
'title' => 'Default',
'navigationTitle' => '',
'flag' => 'us',
'locale' => 'en_US.UTF-8',
],
];
$baseUrl = $this->resolveBaseWithVariants(
$configuration['base'] ?? '',
$configuration['baseVariants'] ?? null
);
$this->base = new Uri($this->sanitizeBaseUrl($baseUrl));
$this->sets = $configuration['dependencies'] ?? [];
foreach ($configuration['languages'] as $languageConfiguration) {
$languageUid = (int)$languageConfiguration['languageId'];
// site language has defined its own base, this is the case most of the time.
if (!empty($languageConfiguration['base'])) {
$base = $this->resolveBaseWithVariants(
$languageConfiguration['base'],
$languageConfiguration['baseVariants'] ?? null
);
$base = new Uri($this->sanitizeBaseUrl($base));
// no host given by the language-specific base, so lets prefix the main site base
if ($base->getScheme() === '' && $base->getHost() === '') {
$base = rtrim((string)$this->base, '/') . '/' . ltrim((string)$base, '/');
$base = new Uri($this->sanitizeBaseUrl($base));
}
} else {
// Language configuration does not have a base defined
// So the main site base is used (usually done for default languages)
$base = new Uri($this->sanitizeBaseUrl(rtrim((string)$this->base, '/') . '/'));
}
if (!empty($languageConfiguration['flag'])) {
if ($languageConfiguration['flag'] === 'global') {
$languageConfiguration['flag'] = 'flags-multiple';
} elseif ($languageConfiguration['flag'] !== 'empty-empty') {
$languageConfiguration['flag'] = 'flags-' . $languageConfiguration['flag'];
}
}
$this->languages[$languageUid] = new SiteLanguage(
$languageUid,
$languageConfiguration['locale'],
$base,
$languageConfiguration
);
}
foreach ($configuration['errorHandling'] ?? [] as $errorHandlingConfiguration) {
$code = $errorHandlingConfiguration['errorCode'];
unset($errorHandlingConfiguration['errorCode']);
$this->errorHandlers[(int)$code] = $errorHandlingConfiguration;
}
}
/**
* Checks if the base has variants, and takes the first variant which matches an expression.
*/
protected function resolveBaseWithVariants(string $baseUrl, ?array $baseVariants): string
{
if (!empty($baseVariants)) {
$expressionLanguageResolver = GeneralUtility::makeInstance(
Resolver::class,
'site',
[]
);
foreach ($baseVariants as $baseVariant) {
try {
if ((bool)$expressionLanguageResolver->evaluate($baseVariant['condition'])) {
$baseUrl = $baseVariant['base'];
break;
}
} catch (SyntaxError $e) {
// silently fail and do not evaluate
// no logger here, as Site is currently cached and serialized
}
}
}
return $baseUrl;
}
/**
* Gets the identifier of this site,
* mainly used when maintaining / configuring sites.
*/
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* Returns the base URL of this site
*/
public function getBase(): UriInterface
{
return $this->base;
}
/**
* Returns the root page ID of this site
*/
public function getRootPageId(): int
{
return $this->rootPageId;
}
/**
* Returns all available languages of this site
*
* @return array<LanguageRef, SiteLanguage>
*/
public function getLanguages(): array
{
$languages = [];
foreach ($this->languages as $languageId => $language) {
if ($language->enabled()) {
$languages[$languageId] = $language;
}
}
return $languages;
}
/**
* Returns configured sets of this site
*
* @return list<string>
*/
public function getSets(): array
{
return $this->sets;
}
/**
* Returns all available languages of this site, even the ones disabled for frontend usages
*
* @return array<LanguageRef, SiteLanguage>
*/
public function getAllLanguages(): array
{
return $this->languages;
}
/**
* Returns a language of this site, given by the sys_language_uid
*
* @throws \InvalidArgumentException
*/
public function getLanguageById(int $languageId): SiteLanguage
{
if (isset($this->languages[$languageId])) {
return $this->languages[$languageId];
}
// @todo: Turn this into a specific exception to avoid catching \InvalidArgumentException
// since there is no hasLanguageById() or similar and some core places already
// call this method and try-catch global \InvalidArgumentException, which is bad practice.
throw new \InvalidArgumentException(
'Language ' . $languageId . ' does not exist on site ' . $this->identifier . '.',
1522960188
);
}
public function getDefaultLanguage(): SiteLanguage
{
foreach ($this->languages as $language) {
if ($language->isPrimary()) {
return $language;
}
}
return reset($this->languages);
}
/**
* @return array<LanguageRef, SiteLanguage>
*/
public function getAvailableLanguages(BackendUserAuthentication $user, bool $includeAllLanguagesFlag = false, ?int $pageId = null): array
{
$availableLanguages = [];
// Check if we need to add language "-1"
if ($includeAllLanguagesFlag && $user->checkLanguageAccess(-1)) {
$availableLanguages[-1] = new SiteLanguage(-1, '', $this->getBase(), [
'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:multipleLanguages'),
'flag' => 'flags-multiple',
]);
}
// Do not add the ones that are not allowed by the user
foreach ($this->languages as $language) {
if ($user->checkLanguageAccess($language)) {
$availableLanguages[$language->getLanguageId()] = $language;
}
}
return $availableLanguages;
}
/**
* Returns a ready-to-use error handler, to be used within the ErrorController
*
* @throws PageErrorHandlerNotConfiguredException
* @throws InvalidPageErrorHandlerException
*/
public function getErrorHandler(int $statusCode): PageErrorHandlerInterface
{
$errorHandlerConfiguration = $this->errorHandlers[$statusCode] ?? $this->errorHandlers[0] ?? null;
switch ($errorHandlerConfiguration['errorHandler'] ?? null) {
case self::ERRORHANDLER_TYPE_FLUID:
return GeneralUtility::makeInstance(FluidPageErrorHandler::class, $statusCode, $errorHandlerConfiguration);
case self::ERRORHANDLER_TYPE_PAGE:
return GeneralUtility::makeInstance(PageContentErrorHandler::class, $statusCode, $errorHandlerConfiguration);
case self::ERRORHANDLER_TYPE_LOGIN_REDIRECT:
return GeneralUtility::makeInstance(RedirectLoginErrorHandler::class, $statusCode, $errorHandlerConfiguration);
case self::ERRORHANDLER_TYPE_PHP:
$handler = GeneralUtility::makeInstance($errorHandlerConfiguration['errorPhpClassFQCN'], $statusCode, $errorHandlerConfiguration);
// Check if the interface is implemented
if (!($handler instanceof PageErrorHandlerInterface)) {
throw new InvalidPageErrorHandlerException('The configured error handler "' . (string)$errorHandlerConfiguration['errorPhpClassFQCN'] . '" for status code ' . $statusCode . ' must implement the PageErrorHandlerInterface.', 1527432330);
}
return $handler;
}
throw new PageErrorHandlerNotConfiguredException('No error handler given for the status code "' . $statusCode . '".', 1522495914);
}
/**
* Returns the whole configuration for this site
*/
public function getConfiguration(): array
{
return $this->configuration;
}
public function getRawConfiguration(): array
{
return $this->rawConfiguration;
}
public function getSettings(): SiteSettings
{
return $this->settings;
}
/**
* @internal
*/
public function isTypoScriptRoot(): bool
{
return $this->sets !== [] || $this->typoscript !== null || $this->tsConfig !== null;
}
public function getTypoScript(): ?SiteTypoScript
{
return $this->typoscript;
}
/**
* @internal
*/
public function getTSconfig(): ?SiteTSconfig
{
return $this->tsConfig;
}
/**
* Returns a single configuration attribute
*
* @return mixed
* @throws \InvalidArgumentException
*/
public function getAttribute(string $attributeName)
{
if (isset($this->configuration[$attributeName])) {
return $this->configuration[$attributeName];
}
throw new \InvalidArgumentException(
'Attribute ' . $attributeName . ' does not exist on site ' . $this->identifier . '.',
1522495954
);
}
/**
* If a site base contains "/" or "www.domain.com", it is ensured that
* parse_url() can handle this kind of configuration properly.
*/
protected function sanitizeBaseUrl(string $base): string
{
// no protocol ("//") and the first part is no "/" (path), means that this is a domain like
// "www.domain.com/subpage", and we want to ensure that this one then gets a "no-scheme agnostic" part
if (!empty($base) && !str_contains($base, '//') && $base[0] !== '/') {
// either a scheme is added, or no scheme but with domain, or a path which is not absolute
// make the base prefixed with a slash, so it is recognized as path, not as domain
// treat as path
if (!str_contains($base, '.')) {
$base = '/' . $base;
} else {
// treat as domain name
$base = '//' . $base;
}
}
return $base;
}
/**
* Returns the applicable router for this site. This might be configurable in the future.
*/
public function getRouter(?Context $context = null): RouterInterface
{
return GeneralUtility::makeInstance(PageRouter::class, $this, $context);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Entity;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerNotConfiguredException;
interface SiteInterface
{
/**
* Returns the root page ID of this site
*/
public function getRootPageId(): int;
/**
* Returns an identifier for the site / configuration
*/
public function getIdentifier(): string;
/**
* Returns the base URL
*/
public function getBase(): UriInterface;
/**
* Returns all available languages of this site visible in the frontend
*
* @return SiteLanguage[]
*/
public function getLanguages(): array;
/**
* Returns a language of this site, given by the sys_language_uid
*
* @throws \InvalidArgumentException
*/
public function getLanguageById(int $languageId): SiteLanguage;
/**
* Returns the first language that was configured. This is usually language=0
*/
public function getDefaultLanguage(): SiteLanguage;
/**
* Fetch the available languages for a specific backend user, used in various places in Backend and Frontend
* when a Backend User is authenticated.
*
* @param BackendUserAuthentication $user the authenticated backend user to check access rights
* @param bool $includeAllLanguagesFlag whether "-1" should be included in the values or not.
* @param int|null $pageId usually used for resolving additional information from PageTS, only used for pseudo-sites. uid of the default language row!
* @return SiteLanguage[]
*/
public function getAvailableLanguages(BackendUserAuthentication $user, bool $includeAllLanguagesFlag = false, ?int $pageId = null): array;
/**
* Returns a ready-to-use error handler, to be used within the ErrorController
*
* @throws PageErrorHandlerNotConfiguredException
*/
public function getErrorHandler(int $statusCode): PageErrorHandlerInterface;
}
+311
View File
@@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Entity;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Core\Localization\Locale;
/**
* Entity representing a site_language configuration of a site object.
*/
class SiteLanguage
{
/**
* The language id.
*
* @var int
*/
protected $languageId;
/**
* The language tag, like 'de', 'en', 'de-CH'.
*/
protected string $languageTag = '';
/**
* Locale, like 'de-CH' or 'en-GB'
*/
protected Locale $locale;
/**
* The Base URL for this language
*
* @var UriInterface
*/
protected $base;
/**
* Label to be used within TYPO3 to identify the language
* @var string
*/
protected $title = 'Default';
/**
* Label to be used within language menus
* @var string
*/
protected $navigationTitle = '';
/**
* Localized title of the site to be used in title tag.
* @var string
*/
protected $websiteTitle = '';
/**
* The flag key (like "gb" or "fr") used to be used in TYPO3's Backend.
* @var string
*/
protected $flagIdentifier = '';
/**
* Language tag for this language defined by RFC 1766 / 3066 for "hreflang" attribute
*
* @var string
*/
protected $hreflang = '';
/**
* Prefix for TYPO3's language files. If empty, this
* is fetched from $locale
*
* @var string
*/
protected $typo3Language = '';
/**
* @var string
*/
protected $fallbackType = 'strict';
/**
* @var array
*/
protected $fallbackLanguageIds = [];
/**
* @var bool
*/
protected $enabled = true;
/**
* Whether this language is the primary language of the site.
*/
protected bool $primary = false;
/**
* Additional parameters configured for this site language
* @var array
*/
protected $configuration = [];
/**
* SiteLanguage constructor.
*/
public function __construct(int $languageId, string $locale, UriInterface $base, array $configuration)
{
$this->languageId = $languageId;
$this->locale = new Locale($locale);
$this->base = $base;
$this->configuration = $configuration;
if (!empty($configuration['languageTag'])) {
$this->languageTag = $configuration['languageTag'];
}
if (!empty($configuration['title'])) {
$this->title = $configuration['title'];
}
if (!empty($configuration['navigationTitle'])) {
$this->navigationTitle = $configuration['navigationTitle'];
}
if (!empty($configuration['websiteTitle'])) {
$this->websiteTitle = $configuration['websiteTitle'];
}
if (!empty($configuration['flag'])) {
$this->flagIdentifier = $configuration['flag'];
}
if (!empty($configuration['typo3Language'])) {
$this->typo3Language = $configuration['typo3Language'];
}
if (!empty($configuration['hreflang'])) {
$this->hreflang = $configuration['hreflang'];
}
if (!empty($configuration['fallbackType'])) {
$this->fallbackType = $configuration['fallbackType'];
}
if (isset($configuration['fallbacks'])) {
$fallbackLanguageIds = $configuration['fallbacks'];
// It is important to distinct between "0" and "" so, empty() should not be used here
if (is_string($fallbackLanguageIds)) {
if ($fallbackLanguageIds !== '') {
$fallbackLanguageIds = explode(',', $fallbackLanguageIds);
} else {
$fallbackLanguageIds = [];
}
} elseif (is_scalar($fallbackLanguageIds)) {
$fallbackLanguageIds = [$fallbackLanguageIds];
}
$this->fallbackLanguageIds = array_map(intval(...), $fallbackLanguageIds);
}
if (isset($configuration['enabled'])) {
$this->enabled = (bool)$configuration['enabled'];
}
if (!empty($configuration['primary'])) {
$this->primary = true;
}
}
/**
* Returns the SiteLanguage in an array representation for e.g. the usage
* in TypoScript.
*/
public function toArray(): array
{
return array_merge($this->configuration, [
'languageId' => $this->getLanguageId(),
'languageTag' => $this->getLanguageTag(),
// kept for backwards-compat for the time being, might change to BGP-47 format
'locale' => $this->getLocale()->posixFormatted(),
'base' => (string)$this->getBase(),
'title' => $this->getTitle(),
'websiteTitle' => $this->getWebsiteTitle(),
'navigationTitle' => $this->getNavigationTitle(),
'hreflang' => $this->hreflang ?: $this->locale->getName(),
'typo3Language' => $this->getTypo3Language(),
'flagIdentifier' => $this->getFlagIdentifier(),
'fallbackType' => $this->getFallbackType(),
'enabled' => $this->enabled(),
'primary' => $this->isPrimary(),
'fallbackLanguageIds' => $this->getFallbackLanguageIds(),
]);
}
public function getConfiguration(): array
{
return $this->toArray();
}
public function getLanguageId(): int
{
return $this->languageId;
}
public function getLanguageTag(): string
{
return $this->languageTag;
}
public function getLocale(): Locale
{
return $this->locale;
}
public function getBase(): UriInterface
{
return $this->base;
}
public function getTitle(): string
{
return $this->title;
}
public function getNavigationTitle(): string
{
return $this->navigationTitle ?: $this->title;
}
public function getWebsiteTitle(): string
{
return $this->websiteTitle;
}
public function getFlagIdentifier(): string
{
return $this->flagIdentifier;
}
/**
* Returns the XLF label language key.
* For locales like "en-US", this method returns "en_US" which can then be used
* for XLF file prefixes properly.
*/
public function getTypo3Language(): string
{
if ($this->typo3Language !== '') {
return $this->typo3Language;
}
$typo3Language = $this->locale->getLanguageCode();
if ($this->locale->getCountryCode()) {
$typo3Language .= '_' . $this->locale->getCountryCode();
}
return $typo3Language;
}
/**
* @internal
*/
public function hasCustomTypo3Language(): bool
{
return $this->typo3Language !== '';
}
/**
* Returns the RFC 1766 / 3066 language tag for hreflang tags
*/
public function getHreflang(bool $fetchCustomSetting = false): string
{
// Ensure to check if a custom attribute is set
if ($fetchCustomSetting) {
return $this->hreflang;
}
return $this->hreflang ?: $this->locale->getName();
}
/**
* Returns true if the language is available in frontend usage
*/
public function enabled(): bool
{
return $this->enabled;
}
/**
* Helper so fluid can work with this as well.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
public function isPrimary(): bool
{
return $this->primary;
}
public function getFallbackType(): string
{
return $this->fallbackType;
}
public function getFallbackLanguageIds(): array
{
return $this->fallbackLanguageIds;
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Entity;
use TYPO3\CMS\Core\Settings\Settings;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* Entity representing all settings for a site. These settings are not overlaid
* with TypoScript settings / constants which happens in the TypoScript Parser
* for a specific page.
*/
final readonly class SiteSettings implements SettingsInterface, \JsonSerializable
{
/**
* @internal to be constructed by create() or createFromSettingsTree()
*/
public function __construct(
private SettingsInterface $settings,
private array $settingsTree,
private array $flattenedArrayValues,
) {}
public function has(string $identifier): bool
{
return $this->settings->has($identifier) || array_key_exists($identifier, $this->settingsTree) || array_key_exists($identifier, $this->flattenedArrayValues);
}
public function isEmpty(): bool
{
return $this->settings->getIdentifiers() === [];
}
public function get(string $identifier, mixed $defaultValue = null): mixed
{
if ($this->settings->has($identifier)) {
return $this->settings->get($identifier);
}
return $this->flattenedArrayValues[$identifier] ?? $this->settingsTree[$identifier] ?? $defaultValue;
}
public function getAll(): array
{
return $this->settingsTree;
}
public function getMap(): array
{
$map = [];
foreach ($this->settings->getIdentifiers() as $key) {
$map[$key] = $this->settings->get($key);
}
return $map;
}
public function getAllFlat(): array
{
return [
...$this->flattenedArrayValues,
...array_filter($this->getMap(), static fn(mixed $value): bool => !is_array($value)),
];
}
/**
* @todo Update jsonSerialize() to return settings map and settings tree values, or remove altogether.
*/
public function jsonSerialize(): mixed
{
return json_encode($this->settingsTree);
}
public function getIdentifiers(): array
{
return $this->settings->getIdentifiers();
}
public static function __set_state(array $state): static
{
return new static(...$state);
}
/**
* @internal
*/
public static function create(SettingsInterface $settings): self
{
$tree = [];
$flattenedArrayValues = [];
foreach ($settings->getIdentifiers() as $key) {
$value = $settings->get($key);
$tree = ArrayUtility::setValueByPath($tree, $key, $value, '.');
if (is_array($value)) {
foreach (ArrayUtility::flattenPlain($value) as $flatKey => $flatValue) {
$flattenedArrayValues[$key . '.' . $flatKey] = $flatValue;
}
}
}
return new self(
settings: $settings,
settingsTree: $tree,
flattenedArrayValues: $flattenedArrayValues,
);
}
/**
* @internal
*/
public static function createFromSettingsTree(array $settingsTree): self
{
$flatSettings = $settingsTree === [] ? [] : ArrayUtility::flattenPlain($settingsTree);
return new self(
settings: new Settings($flatSettings),
settingsTree: $settingsTree,
flattenedArrayValues: [],
);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Entity;
/**
* @internal
*/
final readonly class SiteTSconfig
{
public function __construct(
public ?string $pageTSconfig = null,
) {}
public static function __set_state(array $state): self
{
return new self(...$state);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Entity;
final readonly class SiteTypoScript
{
public function __construct(
public ?string $setup = null,
public ?string $constants = null,
) {}
public static function __set_state(array $state): self
{
return new self(...$state);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
use TYPO3\CMS\Core\Settings\Category;
use TYPO3\CMS\Core\Settings\CategoryAccumulator;
class CategoryRegistry
{
public function __construct(
protected SetRegistry $setRegistry,
) {}
/**
* Retrieve list of instantiated categories for the list of
* provided $setNames, including their dependencies (recursive)
*
* @return list<Category>
*/
public function getCategories(string ...$setNames): array
{
$sets = $this->setRegistry->getSets(...$setNames);
$categories = [];
$categoryDefinitions = [];
foreach ($sets as $set) {
foreach ($set->categoryDefinitions as $definition) {
$categoryDefinitions[$definition->key] = $definition;
}
}
$settingsDefinitions = [];
foreach ($sets as $set) {
foreach ($set->settingsDefinitions as $definition) {
$settingsDefinitions[$definition->key] = $definition;
}
}
$cateryAccumulator = new CategoryAccumulator();
return $cateryAccumulator->getCategories(
$categoryDefinitions,
$settingsDefinitions,
);
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
/**
* @internal Only to be used by internal site settings functionality
*/
final class InvalidCategoryDefinitionsException extends \RuntimeException
{
private readonly string $setName;
public function __construct(
string $message = '',
int $code = 0,
?\Throwable $previous = null,
string $setName = '',
) {
parent::__construct($message, $code, $previous);
$this->setName = $setName;
}
public function getSetName(): string
{
return $this->setName;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
/**
* @internal Only to be used by internal site settings functionality
*/
class InvalidSetException extends \RuntimeException
{
private readonly string $setName;
public function __construct(
string $message = '',
int $code = 0,
?\Throwable $previous = null,
string $setName = '',
) {
parent::__construct($message, $code, $previous);
$this->setName = $setName;
}
public function getSetName(): string
{
return $this->setName;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
/**
* @internal Only to be used by internal site set functionality
*/
final class InvalidSetRouteEnhancersException extends \RuntimeException
{
private readonly string $setName;
public function __construct(
string $message = '',
int $code = 0,
?\Throwable $previous = null,
string $setName = '',
) {
parent::__construct($message, $code, $previous);
$this->setName = $setName;
}
public function getSetName(): string
{
return $this->setName;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
/**
* @internal Only to be used by internal site settings functionality
*/
final class InvalidSettingsDefinitionsException extends \RuntimeException
{
private readonly string $setName;
public function __construct(
string $message = '',
int $code = 0,
?\Throwable $previous = null,
string $setName = '',
) {
parent::__construct($message, $code, $previous);
$this->setName = $setName;
}
public function getSetName(): string
{
return $this->setName;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
/**
* @internal Only to be used by internal site settings functionality
*/
final class InvalidSettingsException extends \RuntimeException
{
private readonly string $setName;
public function __construct(
string $message = '',
int $code = 0,
?\Throwable $previous = null,
string $setName = '',
) {
parent::__construct($message, $code, $previous);
$this->setName = $setName;
}
public function getSetName(): string
{
return $this->setName;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
/**
* @internal
*/
class SetCollector
{
/** @var array<string, SetDefinition> */
protected array $sets = [];
/** @var array<string, array{ error: SetError, name: string, context: string }> */
protected array $invalidSets = [];
/**
* @return array<string, SetDefinition>
*/
public function getSetDefinitions(): array
{
return $this->sets;
}
/**
* @return array<string, array{ error: SetError, name: string, context: string }>
*/
public function getInvalidSets(): array
{
return $this->invalidSets;
}
public function add(SetDefinition $set): void
{
$this->sets[$set->name] = $set;
}
public function addError(SetError $error, string $name, string $context): void
{
$this->invalidSets[$name] = [
'error' => $error,
'name' => $name,
'context' => $context,
];
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
use TYPO3\CMS\Core\Settings\CategoryDefinition;
use TYPO3\CMS\Core\Settings\SettingDefinition;
readonly class SetDefinition
{
/**
* @param list<string> $dependencies
* @param SettingDefinition[] $settingsDefinitions
* @param CategoryDefinition[] $categoryDefinitions
* @param array<string, array<string, mixed>> $routeEnhancers Route enhancers keyed by identifier
*/
public function __construct(
public string $name,
public string $label,
public array $dependencies = [],
public array $optionalDependencies = [],
public array $settingsDefinitions = [],
public array $categoryDefinitions = [],
public ?string $typoscript = null,
public ?string $pagets = null,
public array $settings = [],
public bool $hidden = false,
public array $routeEnhancers = [],
) {}
public function toArray(): array
{
return array_filter(get_object_vars($this), fn(mixed $value) => $value !== null && $value !== []);
}
public static function __set_state(array $state): self
{
return new self(...$state);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
enum SetError: string
{
case notFound = 'not-found';
case missingDependency = 'missing-dependency';
case invalidSettingsDefinitions = 'invalid-settings-definitions';
case invalidCategoryDefinitions = 'invalid-category-definitions';
case invalidSettings = 'invalid-settings';
case invalidRouteEnhancers = 'invalid-route-enhancers';
case invalidSet = 'invalid-set';
public function getLabel(): string
{
return match ($this) {
self::notFound => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.siteSet.notFound',
self::missingDependency => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.siteSet.missingDependency',
self::invalidSettingsDefinitions => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.siteSet.invalidSettingsDefinitions',
self::invalidCategoryDefinitions => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.siteSet.invalidCategoryDefinitions',
self::invalidSettings => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.siteSet.invalidSettings',
self::invalidRouteEnhancers => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.siteSet.invalidRouteEnhancers',
self::invalidSet => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.siteSet.invalidSet',
};
}
}
+219
View File
@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
#[Autoconfigure(public: true)]
class SetRegistry
{
/** @var list<SetDefinition>|null */
protected ?array $orderedSets = null;
/** @var array<string, array{ error: SetError, name: string, context: string }> */
protected ?array $invalidSets = null;
public function __construct(
protected DependencyOrderingService $dependencyOrderingService,
#[Autowire(expression: 'service("package-dependent-cache-identifier").withPrefix("Sets").toString()')]
protected readonly string $cacheIdentifier,
#[Autowire(service: 'cache.core')]
protected readonly PhpFrontend $cache,
#[Autowire(lazy: true)]
protected SetCollector $setCollector,
protected LoggerInterface $logger,
) {}
/**
* Retrieve list of ordered sets, matched by
* $setNames, including their dependencies (recursive)
*
* @return list<SetDefinition>
*/
public function getSets(string ...$setNames): array
{
return array_values(array_filter(
$this->getOrderedSets(),
fn(SetDefinition $set): bool
=> in_array($set->name, $setNames, true)
|| $this->hasDependency($setNames, $set->name)
));
}
public function hasSet(string $setName): bool
{
return isset($this->getOrderedSets()[$setName]);
}
/**
* @return array<string, SetDefinition>
* @internal
*/
public function getAllSets(): array
{
return $this->getOrderedSets();
}
public function getSet(string $setName): ?SetDefinition
{
return $this->getOrderedSets()[$setName] ?? null;
}
/**
* @return array<string, array{ error: SetError, name: string, context: string }>
*/
public function getInvalidSets(): array
{
// create ordered sets which logs invalidSets as out-of-band data
if ($this->orderedSets === null) {
$this->getOrderedSets();
}
return $this->invalidSets;
}
/**
* @return array<string, SetDefinition>
*/
protected function getOrderedSets(): array
{
return $this->orderedSets ?? $this->getFromCache() ?? $this->computeOrderedSets();
}
/**
* @return array<string, SetDefinition>
*/
protected function getFromCache(): ?array
{
if (!$this->cache->has($this->cacheIdentifier)) {
return null;
}
$setData = null;
try {
$setData = $this->cache->require($this->cacheIdentifier);
} catch (\Error) {
}
if ($setData === false) {
// Cache entry has been removed in the meantime
return null;
}
if (!is_array($setData) || !isset($setData['orderedSets']) || !isset($setData['invalidSets'])) {
throw new \RuntimeException('Invalid "Site Sets" cache entry', 1727809282);
}
$this->orderedSets = $setData['orderedSets'];
$this->invalidSets = $setData['invalidSets'];
return $this->orderedSets;
}
protected function checkMissingDependencies(array $sets, SetDefinition $set): ?string
{
foreach ($set->dependencies as $dependencyName) {
$dependency = $sets[$dependencyName] ?? null;
if ($dependency === null) {
return $dependencyName;
}
$missingSubDependency = $this->checkMissingDependencies($sets, $dependency);
if ($missingSubDependency !== null) {
return $dependencyName . '[' . $missingSubDependency . ']';
}
}
return null;
}
/**
* @return array<string, SetDefinition>
*/
protected function computeOrderedSets(): array
{
$tmp = [];
$this->invalidSets = $this->setCollector->getInvalidSets();
$sets = $this->setCollector->getSetDefinitions();
foreach ($sets as $set) {
$missingDependency = $this->checkMissingDependencies($sets, $set);
if ($missingDependency !== null) {
$this->logger->error('Invalid set "{name}": Missing dependency "{dependency}"', [
'name' => $set->name,
'dependency' => $missingDependency,
]);
$this->invalidSets[$set->name] = [
'error' => SetError::missingDependency,
'name' => $set->name,
'context' => $missingDependency,
];
continue;
}
$tmp[$set->name] = [
'set' => $set,
'after' => $set->dependencies,
'after-resilient' => array_filter($set->optionalDependencies, static fn($dependency) => isset($sets[$dependency])),
];
}
$this->orderedSets = array_map(
static fn(array $data): SetDefinition => $data['set'],
$this->dependencyOrderingService->orderByDependencies($tmp)
);
$setData = [
'orderedSets' => $this->orderedSets,
'invalidSets' => $this->invalidSets,
];
$this->cache->set($this->cacheIdentifier, 'return ' . var_export($setData, true) . ';');
return $this->orderedSets;
}
protected function hasDependency(array $setNames, string $dependency): bool
{
foreach ($setNames as $setName) {
$set = $this->getSet($setName);
if ($set === null) {
continue;
}
if (in_array($dependency, $set->dependencies, true)) {
return true;
}
if (in_array($dependency, $set->optionalDependencies, true)) {
return true;
}
if ($this->hasDependency($set->dependencies, $dependency)) {
return true;
}
if ($this->hasDependency($set->optionalDependencies, $dependency)) {
return true;
}
}
return false;
}
#[AsEventListener('typo3-core/set-registry')]
public function warmupCaches(CacheWarmupEvent $event): void
{
if ($event->hasGroup('system')) {
$this->computeOrderedSets();
}
}
}
@@ -0,0 +1,371 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlParseException;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Settings\CategoryDefinition;
use TYPO3\CMS\Core\Settings\InvalidSettingDefinitionException;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingDefinitionValidation;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
#[Autoconfigure(public: true)]
class YamlSetDefinitionProvider
{
/** @var array<string, SetDefinition> */
protected array $sets = [];
public function __construct(
protected readonly SettingDefinitionValidation $settingDefinitionValidation,
protected readonly YamlFileLoader $yamlFileLoader,
) {}
/**
* @return array<string, SetDefinition>
*/
public function getSetDefinitions(): array
{
return $this->sets;
}
public function addSet(SetDefinition $set): void
{
$this->sets[$set->name] = $set;
}
public function get(\SplFileInfo $fileInfo, ?string $virtualSetPath = null): SetDefinition
{
$filename = GeneralUtility::fixWindowsFilePath($fileInfo->getPathname());
$path = dirname($filename);
$virtualSetPath ??= $path . '/';
// No placeholders or imports processed on purpose
// Use dependencies for shared sets
try {
$set = Yaml::parseFile($filename);
} catch (ParseException $e) {
$source = $virtualSetPath . basename($filename);
throw new InvalidSetException('Failed to parse set definition from "' . $source . '": ' . $e->getMessage(), 1711024370, $e);
}
$setName = $set['name'] ?? '';
$settingsDefinitionsFile = $path . '/settings.definitions.yaml';
if (is_file($settingsDefinitionsFile)) {
try {
$settingsDefinitions = Yaml::parseFile($settingsDefinitionsFile, Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP);
} catch (ParseException $e) {
$source = $virtualSetPath . basename($settingsDefinitionsFile);
throw new InvalidSettingsDefinitionsException(
'Invalid settings definition. Source: ' . $source,
1711024374,
$e,
$setName
);
}
if (!is_object($settingsDefinitions->settings ?? null)) {
$source = $virtualSetPath . basename($settingsDefinitionsFile);
throw new InvalidSettingsDefinitionsException(
'Missing "settings" key in settings definitions. Source: ' . $source,
1711024378,
null,
$setName
);
}
// YAML maps are decoded as objects. Normalize them to arrays so
// settings/category definitions can be handled uniformly below.
$set['settingsDefinitions'] = array_map(
static fn(?object $value): ?array => $value === null ? null : (array)$value,
get_object_vars($settingsDefinitions->settings)
);
$set['categoryDefinitions'] = [];
if (isset($settingsDefinitions->categories)) {
$set['categoryDefinitions'] = array_map(
static fn(?object $value): ?array => $value === null ? null : (array)$value,
get_object_vars($settingsDefinitions->categories)
);
}
}
$settingsFile = $path . '/settings.yaml';
if (is_file($settingsFile)) {
try {
// HEADS UP: YamlFileLoader::PROCESS_PLACEHOLDERS is omitted on purpose and MUST NOT be added.
// Site sets are intended to be self-contained and must not rely on implicit
// dependencies to global (environment) variables.
$settings = $this->yamlFileLoader->load($settingsFile, YamlFileLoader::PROCESS_IMPORTS | YamlFileLoader::ALLOW_EMPTY_FILE);
} catch (YamlParseException $e) {
$source = $virtualSetPath . basename($settingsFile);
throw new InvalidSettingsException('Invalid settings format. Source: ' . $source, 1711024380, $e, $setName);
}
$set['settings'] = $settings;
}
$routeEnhancersFile = $path . '/route-enhancers.yaml';
if (is_file($routeEnhancersFile)) {
try {
$routeEnhancers = $this->yamlFileLoader->load($routeEnhancersFile, YamlFileLoader::PROCESS_IMPORTS | YamlFileLoader::ALLOW_EMPTY_FILE);
} catch (YamlParseException $e) {
$source = $virtualSetPath . basename($routeEnhancersFile);
throw new InvalidSetRouteEnhancersException(
'Invalid route enhancers format. Source: ' . $source,
1764749081,
$e,
$setName
);
}
if ($routeEnhancers !== [] && !is_array($routeEnhancers['routeEnhancers'] ?? null)) {
$source = $virtualSetPath . basename($routeEnhancersFile);
throw new InvalidSetRouteEnhancersException(
'Missing "routeEnhancers" key in route enhancers file. Source: ' . $source,
1764749082,
null,
$setName
);
}
if ($routeEnhancers !== [] && array_keys($routeEnhancers) !== ['routeEnhancers']) {
$source = $virtualSetPath . basename($routeEnhancersFile);
throw new InvalidSetRouteEnhancersException(
'Superfluous keys in route enhancers file. Use "routeEnhancers" as root level key. Source: ' . $source,
1764749083,
null,
$setName
);
}
$set['routeEnhancers'] = $routeEnhancers['routeEnhancers'] ?? [];
}
if (($set['labels'] ?? '') === '') {
if (is_file($path . '/labels.xlf')) {
$set['labels'] = $virtualSetPath . 'labels.xlf';
}
}
return $this->createDefinition($set, $virtualSetPath);
}
protected function createDefinition(array $set, string $basePath): SetDefinition
{
$settingsDefinitions = [];
$labels = $set['labels'] ?? null;
unset($set['labels']);
if ($labels) {
$set['label'] ??= 'LLL:' . $labels . ':label';
}
foreach (($set['settingsDefinitions'] ?? []) as $setting => $options) {
// Cast objects to arrays
if (is_object($options['options'] ?? null)) {
$options['options'] = (array)$options['options'];
}
if (is_array($options['enum'] ?? null)) {
$options['enum'] = array_combine(
$options['enum'],
array_map(
static fn(string|int|float|bool $value): string => sprintf(
'{label}:settings.%s.enum.%s',
$setting,
is_bool($value) ? ($value ? 'true' : 'false') : (string)$value
),
$options['enum']
)
);
} elseif (is_object($options['enum'] ?? null)) {
$options['enum'] = (array)$options['enum'];
}
if (is_array($options['enum'] ?? null)) {
foreach ($options['enum'] as $enumValue => $enumLabel) {
if ($enumLabel === null) {
$options['enum'][$enumValue] = (string)$enumValue;
}
}
}
if (is_object($options['tags'] ?? null)) {
$options['tags'] = array_values((array)$options['tags']);
}
if ($labels) {
$domain = 'LLL:' . $labels . ':';
$options['label'] ??= $domain . 'settings.' . $setting;
$options['description'] ??= $domain . 'settings.description.' . $setting;
if (is_array($options['enum'] ?? null)) {
foreach ($options['enum'] as $enumValue => $enumLabel) {
if (is_string($enumLabel) && str_starts_with($enumLabel, '{label}:')) {
$options['enum'][$enumValue] = $domain . substr($enumLabel, 8);
}
}
}
}
$settingDefinitionData = [...['key' => $setting], ...$options];
try {
$definition = new SettingDefinition(...$settingDefinitionData);
} catch (\Error $e) {
throw new InvalidSettingsDefinitionsException(
'Invalid setting definition "' . $setting . '": ' . json_encode($options) . ' ' . $this->getObjectConstructionErrors($e, SettingDefinition::class, $settingDefinitionData),
1702623312,
$e,
$set['name'] ?? ''
);
}
try {
$this->settingDefinitionValidation->validate($definition);
} catch (InvalidSettingDefinitionException $e) {
throw new InvalidSettingsDefinitionsException(
$e->getMessage(),
1752483401,
$e,
$set['name'] ?? ''
);
}
$settingsDefinitions[] = $definition;
}
$categoryDefinitions = [];
foreach (($set['categoryDefinitions'] ?? []) as $category => $options) {
if ($labels) {
$options['label'] ??= 'LLL:' . $labels . ':categories.' . $category;
$options['description'] ??= 'LLL:' . $labels . ':categories.description.' . $category;
}
try {
$definition = new CategoryDefinition(...[...['key' => $category], ...$options]);
} catch (\Error $e) {
throw new InvalidCategoryDefinitionsException(
'Invalid category definition "' . $category . '": ' . json_encode($options),
1702623313,
$e,
$set['name'] ?? ''
);
}
$categoryDefinitions[] = $definition;
}
foreach (($set['routeEnhancers'] ?? []) as $identifier => $config) {
if (!is_array($config)) {
throw new InvalidSetRouteEnhancersException(
sprintf('Invalid route enhancer definition "%s": expected array, got %s', $identifier, gettype($config)),
1732800002,
null,
$set['name'] ?? ''
);
}
}
$setData = [
...$set,
'settingsDefinitions' => $settingsDefinitions,
'categoryDefinitions' => $categoryDefinitions,
];
$setData['typoscript'] ??= $basePath;
$setData['pagets'] ??= $basePath . 'page.tsconfig';
try {
return new SetDefinition(...$setData);
} catch (\Error $e) {
throw new InvalidSetException(
'Invalid set definition: ' . json_encode($set) . ' ' . $this->getObjectConstructionErrors($e, SetDefinition::class, $setData),
1170859526,
$e,
$set['name'] ?? ''
);
}
}
protected function getObjectConstructionErrors(
\Error $error,
string $className,
array $arguments,
): string {
$reflection = new \ReflectionClass($className);
$constructor = $reflection->getConstructor();
$parameters = $constructor->getParameters();
$missingParameters = [];
$typeErrors = [];
foreach ($parameters as $parameter) {
if (isset($arguments[$parameter->name])) {
$value = $arguments[$parameter->name];
unset($arguments[$parameter->name]);
$type = $parameter->getType();
if (!$this->typeMatches($type, $value)) {
$typeErrors[$parameter->name] = (string)$type;
}
} elseif (!$parameter->isDefaultValueAvailable()) {
$missingParameters[] = $parameter->name;
}
}
$errors = [];
if ($missingParameters !== []) {
$errors[] = 'Missing properties: ' . implode(', ', $missingParameters);
}
if ($arguments !== []) {
$errors[] = 'Invalid properties: ' . implode(', ', array_keys($arguments));
}
if ($typeErrors !== []) {
$errors[] = 'Invalid type: ' . implode(', ', array_keys($typeErrors));
}
if ($errors === []) {
return $error->getMessage();
}
return implode('; ', $errors);
}
protected function typeMatches(
\ReflectionType $type,
mixed $value
): bool {
if ($type->allowsNull() && $value === null) {
return true;
}
if ($type instanceof \ReflectionUnionType) {
foreach ($type->getTypes() as $t) {
if ($this->typeMatches($t, $value)) {
return true;
}
}
return false;
}
if ($type instanceof \ReflectionIntersectionType) {
foreach ($type->getTypes() as $t) {
if (!$this->typeMatches($t, $value)) {
return false;
}
}
return true;
}
if ($type instanceof \ReflectionNamedType) {
$typeName = $type->getName();
$valueType = gettype($value);
if ($valueType === 'object') {
return is_subclass_of($value, $typeName);
}
return $valueType === $typeName;
}
return true;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use TYPO3\CMS\Core\Site\Entity\Site;
/**
* Interface for SiteAware features of TYPO3
*/
interface SiteAwareInterface
{
public function setSite(Site $site): void;
public function getSite(): Site;
}
+137
View File
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationChangedEvent;
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
use TYPO3\CMS\Core\Exception\Page\PageNotFoundException;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
/**
* Is used in backend and frontend for all places where to read / identify sites and site languages.
*/
readonly class SiteFinder
{
private const string CACHE_IDENTIFIER_ROOT_ID_TO_IDENTIFIER = 'sites-root-id-to-identifier';
public function __construct(
private SiteConfiguration $siteConfiguration,
#[Autowire(service: 'cache.runtime')]
private FrontendInterface $runtimeCache,
) {}
/**
* Return a list of all configured sites
*
* @return Site[]
*/
public function getAllSites(bool $useCache = true): array
{
return $this->siteConfiguration->getAllExistingSites($useCache);
}
/**
* Find a site by given root page id
*
* @param int $rootPageId the page ID (default language)
* @throws SiteNotFoundException
* @internal only for usage in some places for managing Site Configuration, might be removed without further notice
*/
public function getSiteByRootPageId(int $rootPageId): Site
{
$mapping = $this->getRootPageIdToIdentifierMapping();
$sites = $this->siteConfiguration->getAllExistingSites();
if (isset($mapping[$rootPageId], $sites[$mapping[$rootPageId]])) {
return $sites[$mapping[$rootPageId]];
}
throw new SiteNotFoundException('No site found for root page id ' . $rootPageId, 1521668882);
}
/**
* Find a site by given identifier
*
* @throws SiteNotFoundException
*/
public function getSiteByIdentifier(string $identifier): Site
{
$sites = $this->siteConfiguration->getAllExistingSites();
if (isset($sites[$identifier])) {
return $sites[$identifier];
}
throw new SiteNotFoundException('No site found for identifier ' . $identifier, 1521716628);
}
/**
* Traverses the rootline of a page up until a Site was found.
*
* @param string|null $mountPointParameter
* @throws SiteNotFoundException
*/
public function getSiteByPageId(int $pageId, ?array $rootLine = null, ?string $mountPointParameter = null): Site
{
if ($pageId === 0) {
// page uid 0 has no root line. We don't need to ask the root line resolver to know that.
$rootLine = [];
}
if (!is_array($rootLine)) {
try {
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId, (string)$mountPointParameter)->get();
} catch (PageNotFoundException) {
// Usually when a page was hidden or disconnected
// This could be improved by handing in a Context object and decide whether hidden pages
// Should be linkable too
$rootLine = [];
}
}
$sites = $this->siteConfiguration->getAllExistingSites();
$mapping = $this->getRootPageIdToIdentifierMapping();
foreach ($rootLine as $pageInRootLine) {
if (isset($mapping[(int)$pageInRootLine['uid']], $sites[$mapping[(int)$pageInRootLine['uid']]])) {
return $sites[$mapping[(int)$pageInRootLine['uid']]];
}
}
throw new SiteNotFoundException('No site found in root line of page ' . $pageId, 1521716622);
}
#[AsEventListener(event: SiteConfigurationChangedEvent::class)]
public function siteConfigurationChanged(): void
{
$this->runtimeCache->remove(self::CACHE_IDENTIFIER_ROOT_ID_TO_IDENTIFIER);
}
private function getRootPageIdToIdentifierMapping(): array
{
$mapping = $this->runtimeCache->get(self::CACHE_IDENTIFIER_ROOT_ID_TO_IDENTIFIER);
if (is_array($mapping)) {
return $mapping;
}
$sites = $this->siteConfiguration->getAllExistingSites();
$mapping = [];
foreach ($sites as $identifier => $site) {
$mapping[$site->getRootPageId()] = $identifier;
}
$this->runtimeCache->set(self::CACHE_IDENTIFIER_ROOT_ID_TO_IDENTIFIER, $mapping);
return $mapping;
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
/**
* Interface for SiteLanguageAware features of TYPO3
*/
interface SiteLanguageAwareInterface
{
public function setSiteLanguage(SiteLanguage $siteLanguage);
public function getSiteLanguage(): SiteLanguage;
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
/**
* Helper trait to use a site language within a class.
*
* @internal this is not public API yet as this might change, and could be changed within TYPO3 Core at any time.
*/
trait SiteLanguageAwareTrait
{
/**
* @var Entity\SiteLanguage
*/
protected $siteLanguage;
public function setSiteLanguage(SiteLanguage $siteLanguage)
{
$this->siteLanguage = $siteLanguage;
}
public function getSiteLanguage(): SiteLanguage
{
return $this->siteLanguage;
}
}
+436
View File
@@ -0,0 +1,436 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
/**
* Provides site language presets
* @internal
*/
class SiteLanguagePresets
{
protected array $presets = [
'af-ZA' => [
'title' => 'Afrikaans',
'navigationTitle' => 'Afrikaans',
'locale' => 'af_ZA',
'base' => '/af/',
'flag' => 'af',
],
'ar-SA' => [
'title' => 'Arabic',
'navigationTitle' => 'العربية',
'locale' => 'ar_SA',
'base' => '/ar/',
'flag' => 'sa',
],
'bs-BA' => [
'title' => 'Bosnian',
'navigationTitle' => 'Bosanski',
'locale' => 'bs_BA',
'base' => '/ba/',
'flag' => 'ba',
],
'bg-BG' => [
'title' => 'Bulgarian',
'navigationTitle' => 'Български',
'locale' => 'bg_BG',
'base' => '/bg/',
'flag' => 'bg',
],
'ca-ES' => [
'title' => 'Catalan',
'navigationTitle' => 'Català',
'locale' => 'ca_ES',
'base' => '/ca/',
'flag' => 'catalonia',
],
'zh-CN' => [
'title' => 'Chinese (Simplified)',
'navigationTitle' => '汉语',
'locale' => 'zh_CN',
'base' => '/cn/',
'flag' => 'cn',
],
'cs-CZ' => [
'title' => 'Czech',
'navigationTitle' => 'Čeština',
'locale' => 'cs_CZ',
'base' => '/cz/',
'flag' => 'cz',
],
'cy-GB' => [
'title' => 'Welsh',
'navigationTitle' => 'Cymraeg',
'locale' => 'cy_GB',
'base' => '/cy/',
'flag' => 'cy',
],
'da-DK' => [
'title' => 'Danish',
'navigationTitle' => 'Dansk',
'locale' => 'da_DK',
'base' => '/da/',
'flag' => 'dk',
],
'de-DE' => [
'title' => 'German',
'navigationTitle' => 'Deutsch',
'locale' => 'de_DE',
'base' => '/de/',
'flag' => 'de',
],
'el-GR' => [
'title' => 'Greek',
'navigationTitle' => 'Ελληνικά',
'locale' => 'el_GR',
'base' => '/gr/',
'flag' => 'gr',
],
'en-US' => [
'title' => 'English',
'navigationTitle' => 'English',
'locale' => 'en_US',
'base' => '/en/',
'flag' => 'en-us-gb',
],
'eo-XX' => [
'title' => 'Esperanto',
'navigationTitle' => 'Esperanto',
'locale' => 'eo_XX',
'base' => '/eo/',
'flag' => 'eo',
],
'es-ES' => [
'title' => 'Spanish',
'navigationTitle' => 'Español',
'locale' => 'es_ES',
'base' => '/es/',
'flag' => 'es',
],
'et-EE' => [
'title' => 'Estonian',
'navigationTitle' => 'Eesti',
'locale' => 'et_EE',
'base' => '/et/',
'flag' => 'ee',
],
'eu-ES' => [
'title' => 'Basque',
'navigationTitle' => 'Euskara',
'locale' => 'eu_ES',
'base' => '/eu/',
'flag' => 'eu',
],
'fa-IR' => [
'title' => 'Persian',
'navigationTitle' => 'فارسی',
'locale' => 'fa_IR',
'base' => '/fa/',
'flag' => 'ir',
],
'fi-FI' => [
'title' => 'Finnish',
'navigationTitle' => 'Suomi',
'locale' => 'fi_FI',
'base' => '/fi/',
'flag' => 'fi',
],
'fo-FO' => [
'title' => 'Faeroese',
'navigationTitle' => 'Føroyskt',
'locale' => 'fo_FO',
'base' => '/fo/',
'flag' => 'fo',
],
'fr-FR' => [
'title' => 'French',
'navigationTitle' => 'Français',
'locale' => 'fr_FR',
'base' => '/fr/',
'flag' => 'fr',
],
'fr-CA' => [
'title' => 'Canadian French',
'navigationTitle' => 'Français canadien',
'locale' => 'fr_CA',
'base' => '/qc/',
'flag' => 'qc',
],
'gl-ES' => [
'title' => 'Galician',
'navigationTitle' => 'Galego',
'locale' => 'gl_ES',
'base' => '/ga/',
'flag' => 'gl',
],
'kl-DK' => [
'title' => 'Greenlandic',
'navigationTitle' => 'Kalaallisut',
'locale' => 'kl_DK',
'base' => '/gl/',
'flag' => 'kl',
],
'he-IL' => [
'title' => 'Hebrew',
'navigationTitle' => 'עברית',
'locale' => 'he_IL',
'base' => '/he/',
'flag' => 'il',
],
'hi-IN' => [
'title' => 'Hindi',
'navigationTitle' => 'हिन्दी',
'locale' => 'hi_IN',
'base' => '/hi/',
'flag' => 'in',
],
'hr-HR' => [
'title' => 'Croatian',
'navigationTitle' => 'Hrvatski',
'locale' => 'hr_HR',
'base' => '/hr/',
'flag' => 'hr',
],
'hu-HU' => [
'title' => 'Hungarian',
'navigationTitle' => 'Magyar',
'locale' => 'hu_HU',
'base' => '/hu/',
'flag' => 'hu',
],
'is-IS' => [
'title' => 'Icelandic',
'navigationTitle' => 'Íslenska',
'locale' => 'is_IS',
'base' => '/is/',
'flag' => 'is',
],
'it-IT' => [
'title' => 'Italian',
'navigationTitle' => 'Italiano',
'locale' => 'it_IT',
'base' => '/it/',
'flag' => 'it',
],
'ja-JP' => [
'title' => 'Japanese',
'navigationTitle' => '日本語',
'locale' => 'ja_JP',
'base' => '/jp/',
'flag' => 'jp',
],
'ka-GE' => [
'title' => 'Georgian',
'navigationTitle' => 'ქართული',
'locale' => 'ka_GE',
'base' => '/ge/',
'flag' => 'ge',
],
'km-KH' => [
'title' => 'Khmer',
'navigationTitle' => 'ភាសាខ្មែរ',
'locale' => 'km_KH',
'base' => '/km/',
'flag' => 'km',
],
'ko-KR' => [
'title' => 'Korean',
'navigationTitle' => '한국말',
'locale' => 'ko_KR',
'base' => '/kr/',
'flag' => 'kr',
],
'lt-LT' => [
'title' => 'Lithuanian',
'navigationTitle' => 'Lietuvių',
'locale' => 'lt_LT',
'base' => '/lt/',
'flag' => 'lt',
],
'lv-LV' => [
'title' => 'Latvian',
'navigationTitle' => 'Latviešu',
'locale' => 'lv_LV',
'base' => '/lv/',
'flag' => 'lv',
],
'mi-NZ' => [
'title' => 'Maori',
'navigationTitle' => 'Māori',
'locale' => 'mi_NZ',
'base' => '/mi/',
'flag' => 'mi',
],
'ms-MY' => [
'title' => 'Malay',
'navigationTitle' => 'Bahasa Melayu',
'locale' => 'ms_MY',
'base' => '/ms/',
'flag' => 'my',
],
'nl-NL' => [
'title' => 'Dutch',
'navigationTitle' => 'Nederlands',
'locale' => 'nl_NL',
'base' => '/nl/',
'flag' => 'nl',
],
'no-NO' => [
'title' => 'Norwegian',
'navigationTitle' => 'Norsk',
'locale' => 'no_NO',
'base' => '/no/',
'flag' => 'no',
],
'pl-PL' => [
'title' => 'Polish',
'navigationTitle' => 'Polski',
'locale' => 'pl_PL',
'base' => '/pl/',
'flag' => 'pl',
],
'pt-PT' => [
'title' => 'Portuguese',
'navigationTitle' => 'Português',
'locale' => 'pt_PT',
'base' => '/pt/',
'flag' => 'pt',
],
'pt-BR' => [
'title' => 'Brazilian Portuguese',
'navigationTitle' => 'Português brasileiro',
'locale' => 'pt_BR',
'base' => '/br/',
'flag' => 'br',
],
'ro-RO' => [
'title' => 'Romanian',
'navigationTitle' => 'Română',
'locale' => 'ro_RO',
'base' => '/ro/',
'flag' => 'ro',
],
'ru-RU' => [
'title' => 'Russian',
'navigationTitle' => 'Русский',
'locale' => 'ru_RU',
'base' => '/ru/',
'flag' => 'ru',
],
'sl-SI' => [
'title' => 'Slovenian',
'navigationTitle' => 'Slovenščina',
'locale' => 'sl_SI',
'base' => '/si/',
'flag' => 'si',
],
'sk-SK' => [
'title' => 'Slovak',
'navigationTitle' => 'Slovenčina',
'locale' => 'sk_SK',
'base' => '/sk/',
'flag' => 'sk',
],
'sn_ZW' => [
'title' => 'Shona (Bantu)',
'navigationTitle' => 'chiShona',
'locale' => 'sn_ZW',
'base' => '/sn/',
'flag' => 'zw',
],
'sv-SE' => [
'title' => 'Swedish',
'navigationTitle' => 'Svenska',
'locale' => 'sv_SE',
'base' => '/se/',
'flag' => 'se',
],
'sq-AL' => [
'title' => 'Albanian',
'navigationTitle' => 'Gjuha shqipe',
'locale' => 'sq_AL',
'base' => '/sq/',
'flag' => 'al',
],
'sr-YO' => [
'title' => 'Serbian',
'navigationTitle' => 'Српски / Srpski',
'locale' => 'sr_YO',
'base' => '/sr/',
'flag' => 'rs',
],
'th-TH' => [
'title' => 'Thai',
'navigationTitle' => 'ภาษาไทย',
'locale' => 'th_TH',
'base' => '/th/',
'flag' => 'th',
],
'tr-TR' => [
'title' => 'Turkish',
'navigationTitle' => 'Türkçe',
'locale' => 'tr_TR',
'base' => '/tr/',
'flag' => 'tr',
],
'uk-UA' => [
'title' => 'Ukrainian',
'navigationTitle' => 'Українська',
'locale' => 'uk_UA',
'base' => '/ua/',
'flag' => 'ua',
],
'vi-VN' => [
'title' => 'Vietnamese',
'navigationTitle' => 'Tiếng Việt',
'locale' => 'vi_VN',
'base' => '/vn/',
'flag' => 'vn',
],
'zh-HK' => [
'title' => 'Chinese (Traditional)',
'navigationTitle' => '漢語',
'locale' => 'zh_HK',
'base' => '/hk/',
'flag' => 'hk',
],
];
public function getAll(): array
{
return $this->presets;
}
public function getPresetDetailsForLanguage(string $language): ?array
{
return $this->presets[$language] ?? null;
}
public function getAllForSelector(): array
{
$presetOptions = [];
foreach ($this->presets as $language => $preset) {
$presetOptions[$preset['title']] = [
'value' => $language,
'label' => $preset['title'],
];
}
ksort($presetOptions);
return $presetOptions;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
use TYPO3\CMS\Core\Settings\Settings;
use TYPO3\CMS\Core\Settings\SettingsFactory;
use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
use TYPO3\CMS\Core\Site\Entity\SiteSettings;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
#[Autoconfigure(public: true)]
readonly class SiteSettingsFactory
{
public function __construct(
#[Autowire('%env(TYPO3:configPath)%/sites')]
protected string $configPath,
protected SetRegistry $setRegistry,
protected SettingsTypeRegistry $settingsTypeRegistry,
protected SettingsFactory $settingsFactory,
protected YamlFileLoader $yamlFileLoader,
#[Autowire(service: 'cache.core')]
protected PhpFrontend $cache,
#[Autowire(expression: 'service("package-dependent-cache-identifier").withPrefix("SiteSettings")')]
protected PackageDependentCacheIdentifier $cacheIdentifier,
protected string $settingsFileName = 'settings.yaml',
) {}
public function getSettings(string $siteIdentifier, array $siteConfiguration): SiteSettings
{
$cacheIdentifier = $this->cacheIdentifier->withAdditionalHashedIdentifier(
$siteIdentifier . '_' . json_encode($siteConfiguration)
)->toString();
try {
$settings = $this->cache->require($cacheIdentifier);
if ($settings instanceof SiteSettings) {
return $settings;
}
} catch (\Error) {
}
$settings = $this->createSettings(
$siteConfiguration['dependencies'] ?? [],
$siteIdentifier,
$siteConfiguration['settings'] ?? [],
);
$this->cache->set($cacheIdentifier, 'return ' . var_export($settings, true) . ';');
return $settings;
}
/**
* Load settings from config/sites/{$siteIdentifier}/settings.yaml.
*/
public function loadLocalSettings(string $siteIdentifier): ?array
{
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->settingsFileName;
if (!file_exists($fileName)) {
return null;
}
return $this->yamlFileLoader->load(
GeneralUtility::fixWindowsFilePath($fileName),
YamlFileLoader::PROCESS_PLACEHOLDERS | YamlFileLoader::PROCESS_IMPORTS | YamlFileLoader::ALLOW_EMPTY_FILE
);
}
/**
* Fetch the settings for a specific site and return the parsed Site Settings object.
*
* @todo This method resolves placeholders during the loading, which is okay as this is only used in context where
* the replacement is needed. However, this may change in the future, for example if loading is needed for
* implementing a GUI for the settings - which should either get a dedicated method or a flag to control if
* placeholder should be resolved during yaml file loading or not. The SiteConfiguration save action currently
* avoid calling this method.
*/
public function createSettings(array $sets = [], ?string $siteIdentifier = null, array $inlineSettings = []): SiteSettings
{
$rawSettings = [];
if ($siteIdentifier !== null) {
$rawSettings = $this->loadLocalSettings($siteIdentifier) ?? $inlineSettings;
}
return $this->composeSettings($rawSettings, $sets);
}
public function composeSettings(array $rawSettings, array $sets): SiteSettings
{
return SiteSettings::create(
$this->settingsFactory->resolveSettings(
...$this->getSettingsProviders($rawSettings, $sets)
)
);
}
/**
* @return SiteSettingsProvider[]
*/
protected function getSettingsProviders(array $settings, array $sets): array
{
$activeSets = [];
if ($sets !== []) {
$activeSets = $this->setRegistry->getSets(...$sets);
}
/** @var SiteSettingsProvider[] $providers */
$providers = [];
foreach ($activeSets as $set) {
$providers[] = new SiteSettingsProvider($set->settings, $set->settingsDefinitions);
}
$providers[] = new SiteSettingsProvider($settings);
return $providers;
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsProviderInterface;
use TYPO3\CMS\Core\Settings\SettingValue;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* @internal
*/
final readonly class SiteSettingsProvider implements SettingsProviderInterface
{
public function __construct(
private array $settings,
private array $definitions = [],
) {}
/**
* @return SettingDefinition[]
*/
public function getDefinitions(): array
{
return $this->definitions;
}
/**
* @return SettingValue[]
*/
public function getProvidedSettings(array $currentDefinitions): array
{
// Obtain default settings
/** @var SettingValue[] $defaultSettings */
$defaultSettings = [];
foreach ($this->definitions as $definition) {
$defaultSettings[] = new SettingValue(
value: $definition->default,
key: $definition->key,
definition: $definition,
);
}
// Obtain defined setting values from map presentation
/** @var SettingValue[] $settings */
$settings = [];
$treeSettings = $this->settings;
foreach ($this->settings as $key => $value) {
$definition = $currentDefinitions[$key] ?? null;
if ($definition !== null) {
$settings[] = new SettingValue(
value: $value,
key: $key,
definition: $definition,
);
// A setting that is defined, is not to be interpreted as an anonymous legacy tree setting
// (otherwise the key would be duplicated, but with dots being escaped)
unset($treeSettings[$key]);
}
}
// Obtain defined setting values from tree presentation
/** @var SettingValue[] $legacySettings */
$legacySettings = [];
foreach ($currentDefinitions as $definition) {
if (!ArrayUtility::isValidPath($treeSettings, $definition->key, '.')) {
continue;
}
$value = ArrayUtility::getValueByPath($treeSettings, $definition->key, '.');
$treeSettings = ArrayUtility::removeByPath($treeSettings, $definition->key, '.');
$legacySettings[] = new SettingValue(
value: $value,
key: $definition->key,
definition: $definition,
);
}
// Derive anonymous setting values from tree by mapping tree nodes to dots
$flatSettings = ArrayUtility::flattenPlain($treeSettings);
foreach ($flatSettings as $key => $value) {
$legacySettings[] = new SettingValue(
value: $value,
key: $key,
definition: null,
);
}
return [
...$defaultSettings,
...$legacySettings,
...$settings,
];
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException;
use TYPO3\CMS\Core\Configuration\SiteWriter;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\Settings;
use TYPO3\CMS\Core\Settings\SettingsDiff;
use TYPO3\CMS\Core\Settings\SettingsFactory;
use TYPO3\CMS\Core\Settings\SettingsInterface;
use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteSettings;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
/**
* @internal
*/
readonly class SiteSettingsService
{
public function __construct(
protected SiteWriter $siteWriter,
#[Autowire(service: 'cache.core')]
protected PhpFrontend $codeCache,
protected SetRegistry $setRegistry,
protected SiteSettingsFactory $siteSettingsFactory,
protected SettingsFactory $settingsFactory,
protected SettingsTypeRegistry $settingsTypeRegistry,
protected FlashMessageService $flashMessageService,
) {}
public function hasSettingsDefinitions(Site $site): bool
{
return count($this->getDefinitions($site)) > 0;
}
public function getUncachedSettings(Site $site): SiteSettings
{
// create a fresh Settings instance instead of using
// $site->getSettings() which may have been loaded from cache
return $this->siteSettingsFactory->createSettings(
$site->getSets(),
$site->getIdentifier(),
$site->getRawConfiguration()['settings'] ?? [],
);
}
public function getSetSettings(Site $site): SettingsInterface
{
return $this->siteSettingsFactory->createSettings($site->getSets());
}
public function getLocalSettings(Site $site): SiteSettings
{
$settings = $this->siteSettingsFactory->createSettings(
$site->getSets(),
$site->getIdentifier(),
$site->getRawConfiguration()['settings'] ?? [],
);
$setSettings = $this->getSetSettings($site);
$localSettings = [];
foreach ($settings->getIdentifiers() as $key) {
$value = $settings->get($key);
if ($setSettings->has($key) && $value === $setSettings->get($key)) {
continue;
}
$localSettings[$key] = $value;
}
return SiteSettings::create(new Settings($localSettings));
}
public function computeSettingsDiff(Site $site, SettingsInterface $newSettings, bool $minify = true): SettingsDiff
{
// Settings from sets only setting values without site-local config/sites/*/settings.yaml applied
$defaultSettings = $minify ? $this->siteSettingsFactory->createSettings($site->getSets(), null) : null;
// Settings from config/sites/*/settings.yaml only (our persistence target)
$localSettings = $this->siteSettingsFactory->loadLocalSettings($site->getIdentifier())
?? $site->getRawConfiguration()['settings'] ?? [];
return SettingsDiff::create(
$localSettings,
$newSettings,
$defaultSettings,
);
}
public function writeSettings(Site $site, array $settings): void
{
try {
$this->siteWriter->writeSettings($site->getIdentifier(), $settings);
} catch (SiteConfigurationWriteException $e) {
$flashMessage = new FlashMessage($e->getMessage(), '', ContextualFeedbackSeverity::ERROR, true);
$defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
// SiteWriter currently does not invalidate the code cache, see #103804
$this->codeCache->flush();
}
/**
* @return array<string, SettingDefinition>
*/
public function getDefinitions(Site $site): array
{
$sets = $this->setRegistry->getSets(...$site->getSets());
$definitions = [];
foreach ($sets as $set) {
foreach ($set->settingsDefinitions as $settingDefinition) {
$definitions[$settingDefinition->key] = $settingDefinition;
}
}
return $definitions;
}
public function createSettingsFromFormData(Site $site, array $settingsMap): SettingsInterface
{
return $this->settingsFactory->createSettingsFromFormData($settingsMap, $this->getDefinitions($site));
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Site\Set\SetError;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
#[Autoconfigure(public: true)]
final readonly class TcaSiteSetCollector
{
public function __construct(
private SetRegistry $setRegistry,
private FlashMessageService $flashMessageService,
) {}
public function populateSiteSets(array &$fieldConfiguration): void
{
$currentValue = $fieldConfiguration['row'][$fieldConfiguration['field']] ?? '';
$selectedSets = $currentValue === '' ? [] : array_fill_keys(GeneralUtility::trimExplode(',', $currentValue), true);
$hiddenSets = GeneralUtility::trimExplode(',', $this->getBackendUser()->getTSConfig()['options.']['sites.']['hideSets'] ?? '', true);
foreach ($this->setRegistry->getAllSets() as $set) {
$hidden = $set->hidden || in_array($set->name, $hiddenSets, true);
if ($hidden && !isset($selectedSets[$set->name])) {
continue;
}
$fieldConfiguration['items'][] = [
'label' => $this->getLanguageService()->sL($set->label) . (
$hidden ? ' (' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden') . ')' : ''
),
'value' => $set->name,
];
unset($selectedSets[$set->name]);
}
$flashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
$languageService = $this->getLanguageService();
foreach ($selectedSets as $invalidSet => $_) {
$reason = $this->setRegistry->getInvalidSets()[$invalidSet] ?? [
'error' => SetError::notFound,
'name' => $invalidSet,
'context' => 'site:' . ($fieldConfiguration['row']['identifier'] ?? ''),
];
$error = sprintf(
$languageService->sL($reason['error']->getLabel()),
$reason['name'],
$reason['context'],
);
$fieldConfiguration['items'][] = [
'label' => sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
$error
),
'value' => $invalidSet,
];
$flashMessage = new FlashMessage(
$error,
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.site.invalidSetDependencies'),
ContextualFeedbackSeverity::ERROR,
false
);
$flashMessageQueue->enqueue($flashMessage);
}
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}