TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user