Files
cms-backend/Classes/Controller/SiteSettingsController.php

381 lines
17 KiB
PHP

<?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\Backend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Dto\Settings\EditableSetting;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Enum\ModuleLayout;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\SetupSettingsViewMode;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Http\ResponseFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Settings\Category;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Set\CategoryRegistry;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Site\SiteSettingsService;
use TYPO3\CMS\Core\SysLog\Action\Setting as SettingAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* View, edit, save site settings. Part of Setup module.
*
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
readonly class SiteSettingsController
{
public function __construct(
protected ComponentFactory $componentFactory,
protected ModuleTemplateFactory $moduleTemplateFactory,
protected SiteFinder $siteFinder,
protected SiteSettingsService $siteSettingsService,
protected SettingsTypeRegistry $settingsTypeRegistry,
protected CategoryRegistry $categoryRegistry,
protected UriBuilder $uriBuilder,
protected PageRenderer $pageRenderer,
protected FlashMessageService $flashMessageService,
protected IconFactory $iconFactory,
protected ResponseFactory $responseFactory,
protected FormProtectionFactory $formProtectionFactory,
) {}
public function editAction(ServerRequestInterface $request): ResponseInterface
{
$moduleData = $request->getAttribute('moduleData');
$mode = SetupSettingsViewMode::tryFrom($moduleData->get('settingsMode') ?? '') ?? SetupSettingsViewMode::BASIC;
$moduleData->set('settingsMode', $mode->value);
$identifier = $request->getQueryParams()['site'] ?? null;
if ($identifier === null) {
throw new \RuntimeException('Site identifier to edit must be set', 1713394528);
}
$returnUrl = GeneralUtility::sanitizeLocalUrl(
(string)($request->getQueryParams()['returnUrl'] ?? ''),
$request
) ?: null;
$overviewUrl = (string)$this->uriBuilder->buildUriFromRoute('site_configuration');
$site = $this->siteFinder->getSiteByIdentifier($identifier);
$view = $this->moduleTemplateFactory->create($request);
$settings = $this->siteSettingsService->getUncachedSettings($site);
$setSettings = $this->siteSettingsService->getSetSettings($site);
$categoryEnhancer = function (Category $category) use (&$categoryEnhancer, $settings, $setSettings): Category {
return new Category(...[
...get_object_vars($category),
'label' => $this->getLanguageService()->sL($category->label),
'description' => $category->description !== null ? $this->getLanguageService()->sL($category->description) : $category->description,
'categories' => array_map($categoryEnhancer, $category->categories),
'settings' => array_map(
fn(SettingDefinition $definition): EditableSetting => new EditableSetting(
definition: $this->resolveSettingLabels($definition),
value: $settings->get($definition->key),
systemDefault: $setSettings->get($definition->key),
typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(),
),
$category->settings
),
]);
};
$categories = array_map(
$categoryEnhancer,
$this->categoryRegistry->getCategories(...$site->getSets())
);
$hasSettings = count($categories) > 0;
$this->addDocHeaderBreadcrumb($view, $site);
$this->addDocHeaderCloseAndSaveButtons($view, $returnUrl ?? $overviewUrl, $hasSettings);
$this->addDocHeaderViewModeButton($view, $site, $mode);
$this->addDocHeaderSiteConfigurationButton($view, $site);
if ($hasSettings) {
$this->addDocHeaderExportButton($view, $mode);
}
// Set shortcut context - reload button is added automatically
$view->getDocHeaderComponent()->setShortcutContext(
'site_configuration.editSettings',
sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:labels.edit'), $site->getIdentifier()),
['site' => $site->getIdentifier()]
);
$view->setLayout(ModuleLayout::NORMAL);
$view->assign('site', $site);
$view->assign('siteTitle', $this->getSiteTitle($site));
$view->assign('rootPageId', $site->getRootPageId());
$view->assign('actionUrl', (string)$this->uriBuilder->buildUriFromRoute('site_configuration.saveSettings', array_filter([
'site' => $site->getIdentifier(),
'returnUrl' => $returnUrl,
])));
$view->assign('returnUrl', $returnUrl);
$view->assign('dumpUrl', (string)$this->uriBuilder->buildUriFromRoute('site_configuration.dumpSettings', ['site' => $site->getIdentifier()]));
$view->assign('categories', $categories);
$view->assign('mode', $mode);
$formProtection = $this->formProtectionFactory->createFromRequest($request);
$view->assign('formToken', $formProtection->generateToken('site_configuration', 'saveSettings'));
return $view->renderResponse('SiteSettings/Edit');
}
private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition
{
$languageService = $this->getLanguageService();
return new SettingDefinition(...[
...get_object_vars($definition),
'label' => $languageService->sL($definition->label),
'description' => $definition->description !== null ? $languageService->sL($definition->description) : null,
'enum' => array_map(
static fn(string|int|float|bool $label): string => $languageService->sL((string)$label),
$definition->enum
),
]);
}
public function saveAction(ServerRequestInterface $request): ResponseInterface
{
$identifier = $request->getQueryParams()['site'] ?? null;
if ($identifier === null) {
throw new \RuntimeException('Site identifier to edit must be set', 1713394529);
}
$site = $this->siteFinder->getSiteByIdentifier($identifier);
$parsedBody = $request->getParsedBody();
$formProtection = $this->formProtectionFactory->createFromRequest($request);
if (!$formProtection->validateToken((string)($parsedBody['formToken'] ?? ''), 'site_configuration', 'saveSettings')) {
return $this->responseFactory
->createResponse(400, 'Invalid request token given')
->withHeader('Location', (string)$this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [
'site' => $site->getIdentifier(),
]));
}
$returnUrl = GeneralUtility::sanitizeLocalUrl(
(string)($parsedBody['returnUrl'] ?? ''),
$request
) ?: null;
$overviewUrl = $this->uriBuilder->buildUriFromRoute('site_configuration');
$CMD = $parsedBody['CMD'] ?? '';
$isSave = $CMD === 'save' || $CMD === 'saveclose';
$isSaveClose = $parsedBody['CMD'] === 'saveclose';
if (!$isSave) {
return new RedirectResponse($returnUrl ?? $overviewUrl);
}
$newSettings = $this->siteSettingsService->createSettingsFromFormData($site, $parsedBody['settings'] ?? []);
$settingsDiff = $this->siteSettingsService->computeSettingsDiff($site, $newSettings);
$this->siteSettingsService->writeSettings($site, $settingsDiff->asArray());
if ($settingsDiff->changes !== [] || $settingsDiff->deletions !== []) {
$this->getBackendUser()->writelog(
Type::SITE,
SettingAction::CHANGE,
SystemLogErrorClassification::MESSAGE,
null,
'Site settings changed for \'%s\': %s',
[$site->getIdentifier(), json_encode($settingsDiff)],
'site'
);
$languageService = $this->getLanguageService();
$message = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:save.message.updated');
$flashMessage = new FlashMessage($message, '', ContextualFeedbackSeverity::OK, true);
$defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
if ($isSaveClose) {
return new RedirectResponse($returnUrl ?? $overviewUrl);
}
$editRoute = $this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', array_filter([
'site' => $site->getIdentifier(),
'returnUrl' => $returnUrl,
], static fn(?string $v): bool => $v !== null));
return new RedirectResponse($editRoute);
}
public function dumpAction(ServerRequestInterface $request): ResponseInterface
{
$identifier = $request->getQueryParams()['site'] ?? null;
if ($identifier === null) {
throw new \RuntimeException('Site identifier to edit must be set', 1724772561);
}
$site = $this->siteFinder->getSiteByIdentifier($identifier);
$parsedBody = $request->getParsedBody();
$specificSetting = (string)($parsedBody['specificSetting'] ?? '');
$minify = $specificSetting !== '' ? false : true;
$newSettings = $this->siteSettingsService->createSettingsFromFormData($site, $parsedBody['settings'] ?? []);
$settingsDiff = $this->siteSettingsService->computeSettingsDiff($site, $newSettings, $minify);
$settings = $settingsDiff->asArray();
if ($specificSetting !== '' && isset($settings[$specificSetting])) {
$settings = [
$specificSetting => $settings[$specificSetting],
];
}
$yamlContents = Yaml::dump($settings, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
return new JsonResponse([
'yaml' => $yamlContents,
]);
}
protected function addDocHeaderBreadcrumb(ModuleTemplate $moduleTemplate, Site $site): void
{
$record = BackendUtility::getRecord('pages', $site->getRootPageId());
$moduleTemplate->getDocHeaderComponent()->setPageBreadcrumb($record ?? []);
}
protected function addDocHeaderCloseAndSaveButtons(ModuleTemplate $moduleTemplate, string $closeUrl, bool $saveEnabled): void
{
$moduleTemplate->addButtonToButtonBar($this->componentFactory->createCloseButton($closeUrl));
$saveButton = $this->componentFactory->createSaveButton('sitesettings_form')
->setName('CMD')
->setValue('save')
->setDisabled(!$saveEnabled);
$moduleTemplate->addButtonToButtonBar($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
}
protected function addDocHeaderViewModeButton(ModuleTemplate $moduleTemplate, Site $site, SetupSettingsViewMode $mode): void
{
$languageService = $this->getLanguageService();
$viewModeButton = $this->componentFactory->createDropDownButton()
->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view'))
->setShowLabelText(true);
$viewModeButton->addItem(
$this->componentFactory->createDropDownRadio()
->setActive(($mode === SetupSettingsViewMode::BASIC))
->setHref(
(string)$this->uriBuilder->buildUriFromRoute(
'site_configuration.editSettings',
array_filter([
'site' => $site->getIdentifier(),
'settingsMode' => SetupSettingsViewMode::BASIC->value,
])
)
)
->setLabel($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_settingseditor.xlf:settingseditor.mode.basic'))
->setIcon($this->iconFactory->getIcon('actions-window', IconSize::SMALL))
);
$viewModeButton->addItem(
$this->componentFactory->createDropDownRadio()
->setActive(($mode === SetupSettingsViewMode::ADVANCED))
->setHref(
(string)$this->uriBuilder->buildUriFromRoute(
'site_configuration.editSettings',
array_filter([
'site' => $site->getIdentifier(),
'settingsMode' => SetupSettingsViewMode::ADVANCED->value,
])
)
)
->setLabel($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_settingseditor.xlf:settingseditor.mode.advanced'))
->setIcon($this->iconFactory->getIcon('actions-window-cog', IconSize::SMALL))
);
$moduleTemplate->addButtonToButtonBar($viewModeButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
}
protected function addDocHeaderExportButton(ModuleTemplate $moduleTemplate, SetupSettingsViewMode $mode): void
{
if ($mode === SetupSettingsViewMode::ADVANCED) {
$languageService = $this->getLanguageService();
$exportButton = $this->componentFactory->createInputButton()
->setTitle($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.yamlExport'))
->setIcon($this->iconFactory->getIcon('actions-database-export', IconSize::SMALL))
->setShowLabelText(true)
->setName('CMD')
->setValue('export')
->setForm('sitesettings_form');
$moduleTemplate->addButtonToButtonBar($exportButton, ButtonBar::BUTTON_POSITION_RIGHT);
}
}
protected function addDocHeaderSiteConfigurationButton(ModuleTemplate $moduleTemplate, Site $site): void
{
$languageService = $this->getLanguageService();
$exportButton = $this->componentFactory->createLinkButton()
->setTitle($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.editSiteConfiguration'))
->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL))
->setShowLabelText(true)
->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.edit', [
'site' => $site->getIdentifier(),
'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.editSettings', [
'site' => $site->getIdentifier(),
]),
]));
$moduleTemplate->addButtonToButtonBar($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
}
protected function getSiteTitle(Site $site): string
{
$websiteTitle = $site->getConfiguration()['websiteTitle'] ?? '';
if ($websiteTitle !== '') {
return $websiteTitle;
}
$rootPage = BackendUtility::getRecord('pages', $site->getRootPageId());
$title = $rootPage['title'] ?? '';
if ($title !== '') {
return $title;
}
return '(unknown)';
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}