TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
|
||||
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||
use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
|
||||
|
||||
/**
|
||||
* Controller abstract for shared parts of the install tool.
|
||||
*
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class AbstractController
|
||||
{
|
||||
/**
|
||||
* Helper method to initialize a view instance.
|
||||
*/
|
||||
protected function initializeView(ServerRequestInterface $request): ViewInterface
|
||||
{
|
||||
$templatePaths = [
|
||||
'templateRootPaths' => ['EXT:install/Resources/Private/Templates'],
|
||||
'partialRootPaths' => ['EXT:install/Resources/Private/Partials'],
|
||||
'layoutRootPaths' => ['EXT:install/Resources/Private/Layouts'],
|
||||
];
|
||||
$renderingContext = GeneralUtility::makeInstance(RenderingContextFactory::class)->create($templatePaths, $request);
|
||||
$fluidView = new FluidTemplateView($renderingContext);
|
||||
$view = new FluidViewAdapter($fluidView);
|
||||
$view->assignMultiple([
|
||||
'controller' => $request->getQueryParams()['install']['controller'] ?? 'maintenance',
|
||||
'context' => $request->getQueryParams()['install']['context'] ?? 'install',
|
||||
'composerMode' => Environment::isComposerMode(),
|
||||
'currentTypo3Version' => (string)(new Typo3Version()),
|
||||
'colorScheme' => $request->getQueryParams()['install']['colorScheme'] ?? '',
|
||||
'theme' => $request->getQueryParams()['install']['theme'] ?? '',
|
||||
'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '',
|
||||
]);
|
||||
return $view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
|
||||
use TYPO3\CMS\Install\Service\SessionService;
|
||||
|
||||
/**
|
||||
* Backend module controller to the Install Tool. Sets an Install Tool session
|
||||
* marked as "initialized by a valid system administrator backend user" and
|
||||
* redirects to the Install Tool entry point.
|
||||
*
|
||||
* This is a classic backend module that does not interfere with other code
|
||||
* within the Install Tool, it can be seen as a facade around Install Tool just
|
||||
* to embed the Install Tool in backend.
|
||||
*
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
readonly class BackendModuleController
|
||||
{
|
||||
public function __construct(
|
||||
protected ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected SessionService $sessionService,
|
||||
#[Autowire(expression: 'service("session-manager").getSessionBackend("BE")')]
|
||||
protected SessionBackendInterface $sessionBackend,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initialize session and redirect to "maintenance"
|
||||
*/
|
||||
public function maintenanceAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->setAuthorizedAndRedirect('maintenance', $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize session and redirect to "settings"
|
||||
*/
|
||||
public function settingsAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->setAuthorizedAndRedirect('settings', $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize session and redirect to "upgrade"
|
||||
*/
|
||||
public function upgradeAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->setAuthorizedAndRedirect('upgrade', $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize session and redirect to "environment"
|
||||
*/
|
||||
public function environmentAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->setAuthorizedAndRedirect('environment', $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts / updates the session and redirects to the Install Tool
|
||||
* with given action.
|
||||
*/
|
||||
protected function setAuthorizedAndRedirect(string $controller, ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$redirectParameters = [
|
||||
'install' => [
|
||||
'controller' => $controller,
|
||||
'context' => 'backend',
|
||||
],
|
||||
];
|
||||
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userTS = $backendUser->getTSConfig();
|
||||
|
||||
$themeDisabled = $userTS['setup.']['fields.']['theme.']['disabled'] ?? '0';
|
||||
$theme = $GLOBALS['BE_USER']->uc['theme'] ?? $userTS['setup.']['fields.']['theme'] ?? 'auto';
|
||||
if ($themeDisabled === '1') {
|
||||
$theme = $userTS['setup.']['fields.']['theme'] ?? 'modern';
|
||||
}
|
||||
if ($theme !== 'modern') {
|
||||
$redirectParameters['install']['theme'] = $theme;
|
||||
}
|
||||
|
||||
$colorSchemeDisabled = $userTS['setup.']['fields.']['colorScheme.']['disabled'] ?? '0';
|
||||
$colorScheme = $GLOBALS['BE_USER']->uc['colorScheme'] ?? $userTS['setup.']['fields.']['colorScheme'] ?? 'auto';
|
||||
if ($colorSchemeDisabled === '1') {
|
||||
$colorScheme = $userTS['setup.']['fields.']['colorScheme'] ?? 'light';
|
||||
}
|
||||
if ($colorScheme !== 'auto') {
|
||||
$redirectParameters['install']['colorScheme'] = $colorScheme;
|
||||
}
|
||||
|
||||
$userSession = $this->getBackendUser()->getSession();
|
||||
$this->sessionService->installSessionHandler($request);
|
||||
$this->sessionService->startSession();
|
||||
$this->sessionService->setAuthorizedBackendSession($userSession, $this->sessionBackend);
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$redirectLocation = $normalizedParams->getSiteUrl() . '?__typo3_install&' . http_build_query($redirectParameters, '', '&', PHP_QUERY_RFC3986);
|
||||
return new RedirectResponse($redirectLocation, 303);
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal This class is a specific implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
trait ControllerTrait
|
||||
{
|
||||
/**
|
||||
* Using fixed Content-Security-Policy for Admin Tool (extensions and database might not be available)
|
||||
*/
|
||||
protected function createContentSecurityPolicy(): Policy
|
||||
{
|
||||
return GeneralUtility::makeInstance(Policy::class)
|
||||
->default(SourceKeyword::self)
|
||||
// script-src 'nonce-...' required for importmaps
|
||||
->extend(Directive::ScriptSrc, SourceKeyword::nonceProxy)
|
||||
// `style-src 'unsafe-inline'` required for lit in safari and firefox to allow inline <style> tags
|
||||
// (for browsers that do not support https://caniuse.com/mdn-api_shadowroot_adoptedstylesheets)
|
||||
->extend(Directive::StyleSrc, SourceKeyword::unsafeInline)
|
||||
->set(Directive::StyleSrcAttr, SourceKeyword::unsafeInline)
|
||||
->extend(Directive::ImgSrc, SourceScheme::data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Http\Uri;
|
||||
|
||||
class EntryPointRedirectController
|
||||
{
|
||||
public function redirectAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
return new RedirectResponse(
|
||||
new Uri($normalizedParams->getSiteUrl() . '?__typo3_install')
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Imaging\IconState;
|
||||
|
||||
/**
|
||||
* Controller for icon handling
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class IconController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly IconFactory $iconFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getIconAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$requestedIcon = json_decode($parsedBody['icon'] ?? $queryParams['icon'], true);
|
||||
|
||||
[$identifier, $size, $overlayIdentifier, $iconState, $alternativeMarkupIdentifier] = $requestedIcon;
|
||||
|
||||
if (empty($overlayIdentifier)) {
|
||||
$overlayIdentifier = null;
|
||||
}
|
||||
|
||||
$iconState = IconState::tryFrom($iconState);
|
||||
$size = IconSize::tryFrom($size);
|
||||
$icon = $this->iconFactory->getIcon($identifier, $size, $overlayIdentifier, $iconState);
|
||||
|
||||
return new HtmlResponse($icon->render($alternativeMarkupIdentifier));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Routing\RouteRedirect;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Authentication\CommandLineUserCreation;
|
||||
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||
use TYPO3\CMS\Core\Core\BootService;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
||||
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Type\Map;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
|
||||
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||
use TYPO3\CMS\Install\Factory\ImportMapFactory;
|
||||
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
|
||||
use TYPO3\CMS\Install\Service\EnableFileService;
|
||||
use TYPO3\CMS\Install\Service\Exception\ConfigurationDirectoryDoesNotExistException;
|
||||
use TYPO3\CMS\Install\Service\SetupDatabaseService;
|
||||
use TYPO3\CMS\Install\Service\SetupService;
|
||||
use TYPO3\CMS\Install\SystemEnvironment\Check;
|
||||
use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
|
||||
use TYPO3\CMS\Install\WebserverType;
|
||||
use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
|
||||
|
||||
/**
|
||||
* Install step controller, dispatcher class of step actions.
|
||||
*
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
* @phpstan-import-type Params from DriverManager
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class InstallerController
|
||||
{
|
||||
use ControllerTrait;
|
||||
|
||||
public function __construct(
|
||||
private BootService $bootService,
|
||||
private ConfigurationManager $configurationManager,
|
||||
private PackageManager $packageManager,
|
||||
private VerifyHostHeader $verifyHostHeader,
|
||||
private FormProtectionFactory $formProtectionFactory,
|
||||
private SetupService $setupService,
|
||||
private SetupDatabaseService $setupDatabaseService,
|
||||
private ImportMapFactory $importMapFactory,
|
||||
private HashService $hashService,
|
||||
private IconRegistry $iconRegistry,
|
||||
private DirectiveHashCollection $directiveHashCollection,
|
||||
private CommandLineUserCreation $commandLineUserCreation,
|
||||
private UriBuilder $uriBuilder,
|
||||
private RenderingContextFactory $renderingContextFactory,
|
||||
private ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Init action loads <head> with JS initiating further stuff
|
||||
*/
|
||||
public function initAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$bust = $GLOBALS['EXEC_TIME'];
|
||||
if (!Environment::getContext()->isDevelopment()) {
|
||||
$bust = $this->hashService->hmac((new Typo3Version()) . Environment::getProjectPath(), self::class);
|
||||
}
|
||||
$sitePath = $request->getAttribute('normalizedParams')->getSitePath();
|
||||
$importMap = $this->importMapFactory->create($sitePath);
|
||||
$initModule = $importMap->resolveImport('@typo3/install/init-installer.js', true, $sitePath);
|
||||
$view = $this->initializeView($request);
|
||||
$view->assign('bust', $bust);
|
||||
$view->assign('initModule', $initModule);
|
||||
$view->assign('iconCacheIdentifier', sha1($this->iconRegistry->getBackendIconsCacheIdentifier()));
|
||||
$nonce = new ConsumableNonce();
|
||||
$view->assign('importmap', $importMap->render($sitePath, $nonce));
|
||||
|
||||
return new HtmlResponse(
|
||||
$view->render('Installer/Init'),
|
||||
200,
|
||||
[
|
||||
'Content-Security-Policy' => $this->createContentSecurityPolicy()->compile(new PolicyBag(Scope::backend(), new Map(), new Behavior(), $nonce, $this->directiveHashCollection)),
|
||||
'Cache-Control' => 'no-cache, no-store',
|
||||
'Pragma' => 'no-cache',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main layout with progress bar, header
|
||||
*/
|
||||
public function mainLayoutAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Installer/MainLayout'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render "FIRST_INSTALL file need to exist" view
|
||||
*/
|
||||
public function showInstallerNotAvailableAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Installer/ShowInstallerNotAvailable'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if "environment and folders" should be shown
|
||||
*/
|
||||
public function checkEnvironmentAndFoldersAction(): ResponseInterface
|
||||
{
|
||||
return new JsonResponse([
|
||||
'success' => @is_file($this->configurationManager->getSystemConfigurationFileLocation()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render "environment and folders"
|
||||
*/
|
||||
public function showEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$systemCheckMessageQueue = new FlashMessageQueue('install');
|
||||
$checkMessages = (new Check())->getStatus();
|
||||
foreach ($checkMessages as $message) {
|
||||
$systemCheckMessageQueue->enqueue($message);
|
||||
}
|
||||
$setupCheckMessages = (new SetupCheck())->getStatus();
|
||||
foreach ($setupCheckMessages as $message) {
|
||||
$systemCheckMessageQueue->enqueue($message);
|
||||
}
|
||||
$folderStructureFactory = new DefaultFactory();
|
||||
$structureFacade = $folderStructureFactory->getStructure(WebserverType::fromRequest($request));
|
||||
$structureMessageQueue = $structureFacade->getStatus();
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Installer/ShowEnvironmentAndFolders'),
|
||||
'environmentStatusErrors' => $systemCheckMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR),
|
||||
'environmentStatusWarnings' => $systemCheckMessageQueue->getAllMessages(ContextualFeedbackSeverity::WARNING),
|
||||
'structureErrors' => $structureMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create main folder layout, LocalConfiguration, PackageStates
|
||||
*/
|
||||
public function executeEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$errorsFromStructure = $this->setupService->createDirectoryStructure(WebserverType::fromRequest($request));
|
||||
try {
|
||||
$this->setupService->prepareSystemSettings();
|
||||
} catch (ConfigurationDirectoryDoesNotExistException) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => $errorsFromStructure,
|
||||
]);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if trusted hosts pattern needs to be adjusted
|
||||
*/
|
||||
public function checkTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$serverParams = $request->getServerParams();
|
||||
$host = $serverParams['HTTP_HOST'] ?? '';
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => $this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust trusted hosts pattern to '.*' if it does not match yet
|
||||
*/
|
||||
public function executeAdjustTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$serverParams = $request->getServerParams();
|
||||
$host = $serverParams['HTTP_HOST'] ?? '';
|
||||
|
||||
if (!$this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams)) {
|
||||
$this->configurationManager->setLocalConfigurationValueByPath('SYS/trustedHostsPattern', '.*');
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database connect step needs to be shown
|
||||
*/
|
||||
public function checkDatabaseConnectAction(): ResponseInterface
|
||||
{
|
||||
return new JsonResponse([
|
||||
'success' => $this->setupDatabaseService->isDatabaseConfigurationComplete() && $this->setupDatabaseService->isDatabaseConnectSuccessful(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show database connect step
|
||||
*/
|
||||
public function showDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
|
||||
$driverOptions = $this->setupDatabaseService->getDriverOptions();
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$driverOptions['executeDatabaseConnectToken'] = $formProtection->generateToken('installTool', 'executeDatabaseConnect');
|
||||
$view->assignMultiple($driverOptions);
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Installer/ShowDatabaseConnect'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test database connect data
|
||||
*/
|
||||
public function executeDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$postValues = $request->getParsedBody()['install']['values'];
|
||||
[$success, $messages] = $this->setupDatabaseService->setDefaultConnectionSettings($postValues);
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => $success,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a database needs to be selected
|
||||
*/
|
||||
public function checkDatabaseSelectAction(): ResponseInterface
|
||||
{
|
||||
return new JsonResponse([
|
||||
'success' => $this->setupDatabaseService->checkDatabaseSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render "select a database"
|
||||
*/
|
||||
public function showDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$errors = [];
|
||||
try {
|
||||
$view->assign('databaseList', $this->setupDatabaseService->getDatabaseList());
|
||||
} catch (\Exception $exception) {
|
||||
$errors[] = $exception->getMessage();
|
||||
}
|
||||
$view->assignMultiple([
|
||||
'errors' => $errors,
|
||||
'executeDatabaseSelectToken' => $formProtection->generateToken('installTool', 'executeDatabaseSelect'),
|
||||
'executeCheckDatabaseRequirementsToken' => $formProtection->generateToken('installTool', 'checkDatabaseRequirements'),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Installer/ShowDatabaseSelect'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-check whether all requirements for the installed database driver and platform are fulfilled
|
||||
*/
|
||||
public function checkDatabaseRequirementsAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$success = true;
|
||||
$messages = [];
|
||||
$databaseDriverName = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['driver'];
|
||||
|
||||
$databaseName = $this->retrieveDatabaseNameFromRequest($request);
|
||||
if ($databaseName === '') {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => [
|
||||
new FlashMessage(
|
||||
'You must select a database.',
|
||||
'No Database selected',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname'] = $databaseName;
|
||||
|
||||
foreach ($this->setupDatabaseService->checkDatabaseRequirementsForDriver($databaseDriverName) as $message) {
|
||||
if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) {
|
||||
$success = false;
|
||||
$messages[] = $message;
|
||||
}
|
||||
}
|
||||
|
||||
// Check create and drop permissions
|
||||
$statusMessages = [];
|
||||
foreach ($this->setupDatabaseService->checkRequiredDatabasePermissions() as $checkRequiredPermission) {
|
||||
$statusMessages[] = new FlashMessage(
|
||||
$checkRequiredPermission,
|
||||
'Missing required permissions',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
}
|
||||
if ($statusMessages !== []) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => $statusMessages,
|
||||
]);
|
||||
}
|
||||
|
||||
// if requirements are not fulfilled
|
||||
if ($success === false) {
|
||||
// remove the database again if we created it
|
||||
if ($request->getParsedBody()['install']['values']['type'] === 'new') {
|
||||
$connection = $this->connectionPool
|
||||
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
|
||||
$connection
|
||||
->createSchemaManager()
|
||||
->dropDatabase($connection->quoteIdentifier($databaseName));
|
||||
}
|
||||
|
||||
$this->configurationManager->removeLocalConfigurationKeysByPath(['DB/Connections/Default/dbname']);
|
||||
|
||||
$message = new FlashMessage(
|
||||
sprintf(
|
||||
'Database with name "%s" has been removed due to the following errors. '
|
||||
. 'Please solve them first and try again. If you tried to create a new database make also sure, that the DBMS charset is to use UTF-8',
|
||||
$databaseName
|
||||
),
|
||||
'',
|
||||
ContextualFeedbackSeverity::INFO
|
||||
);
|
||||
array_unshift($messages, $message);
|
||||
}
|
||||
|
||||
unset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname']);
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => $success,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
private function retrieveDatabaseNameFromRequest(ServerRequestInterface $request): string
|
||||
{
|
||||
$postValues = $request->getParsedBody()['install']['values'];
|
||||
if ($postValues['type'] === 'new') {
|
||||
return $postValues['new'];
|
||||
}
|
||||
|
||||
if ($postValues['type'] === 'existing' && !empty($postValues['existing'])) {
|
||||
return $postValues['existing'];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Select / create and test a database
|
||||
*/
|
||||
public function executeDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$databaseName = $this->retrieveDatabaseNameFromRequest($request);
|
||||
if ($databaseName === '') {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => [
|
||||
new FlashMessage(
|
||||
'You must select a database.',
|
||||
'No Database selected',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$postValues = $request->getParsedBody()['install']['values'];
|
||||
if ($postValues['type'] === 'new') {
|
||||
$status = $this->setupDatabaseService->createNewDatabase($databaseName);
|
||||
if ($status->getSeverity() === ContextualFeedbackSeverity::ERROR) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => [$status],
|
||||
]);
|
||||
}
|
||||
} elseif ($postValues['type'] === 'existing') {
|
||||
$status = $this->setupDatabaseService->checkExistingDatabase($databaseName);
|
||||
if ($status->getSeverity() === ContextualFeedbackSeverity::ERROR) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => [$status],
|
||||
]);
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if initial data needs to be imported
|
||||
*/
|
||||
public function checkDatabaseDataAction(): ResponseInterface
|
||||
{
|
||||
$existingTables = $this->connectionPool
|
||||
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
|
||||
->createSchemaManager()
|
||||
->listTableNames();
|
||||
return new JsonResponse([
|
||||
'success' => !empty($existingTables),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render "import initial data"
|
||||
*/
|
||||
public function showDatabaseDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view->assignMultiple([
|
||||
'executeDatabaseDataToken' => $formProtection->generateToken('installTool', 'executeDatabaseData'),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Installer/ShowDatabaseData'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create main db layout
|
||||
*/
|
||||
public function executeDatabaseDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$messages = [];
|
||||
$postValues = $request->getParsedBody()['install']['values'];
|
||||
$username = (string)$postValues['username'] !== '' ? $postValues['username'] : 'admin';
|
||||
// Check password and return early if not good enough
|
||||
$password = (string)($postValues['password'] ?? '');
|
||||
$email = $postValues['email'] ?? '';
|
||||
$passwordValidationErrors = $this->setupDatabaseService->getBackendUserPasswordValidationErrors($password);
|
||||
if (!empty($passwordValidationErrors)) {
|
||||
$messages[] = new FlashMessage(
|
||||
'Administrator password not secure enough!',
|
||||
'',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
|
||||
// Add all password validation errors to the messages array
|
||||
foreach ($passwordValidationErrors as $error) {
|
||||
$messages[] = new FlashMessage(
|
||||
$error,
|
||||
'',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
// Set site name
|
||||
if (!empty($postValues['sitename'])) {
|
||||
$this->setupService->setSiteName($postValues['sitename']);
|
||||
}
|
||||
try {
|
||||
$messages = $this->setupDatabaseService->importDatabaseData();
|
||||
if (!empty($messages)) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
} catch (StatementException $exception) {
|
||||
$messages[] = new FlashMessage(
|
||||
'Error detected in SQL statement:' . LF . $exception->getMessage(),
|
||||
'Import of database data could not be performed',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->commandLineUserCreation->ensureCliUserExists();
|
||||
$this->setupService->createUser($username, $password, $email);
|
||||
$this->setupService->setInstallToolPassword($password);
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show last "create site with theme / install distribution"
|
||||
*/
|
||||
public function showDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$distributions = [];
|
||||
if ($this->packageManager->isPackageActive('impexp')) {
|
||||
$distributions = $this->setupService->getAvailableDistributions();
|
||||
}
|
||||
$view->assignMultiple([
|
||||
'composerMode' => Environment::isComposerMode(),
|
||||
'offerToCreateBasicSite' => $this->packageManager->isPackageActive('fluid_styled_content'),
|
||||
'distributions' => $distributions,
|
||||
'executeDefaultConfigurationToken' => $formProtection->generateToken('installTool', 'executeDefaultConfiguration'),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Installer/ShowDefaultConfiguration'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Last step execution: clean up, remove FIRST_INSTALL file, ...
|
||||
*/
|
||||
public function executeDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Let the admin user redirect to the distributions page on first login
|
||||
$siteSetup = $request->getParsedBody()['install']['values']['sitesetup'] ?? '';
|
||||
$selectedDistribution = '';
|
||||
if (str_starts_with($siteSetup, 'createsite:')) {
|
||||
$selectedDistribution = substr($siteSetup, strlen('createsite:'));
|
||||
$siteSetup = 'activateDistribution';
|
||||
}
|
||||
// It is crucial to activate the package *before* loading the container
|
||||
if ($siteSetup === 'activateDistribution') {
|
||||
// Distribution handles all site creation (pages, content, site configuration)
|
||||
$this->setupService->activateDistributionPackage($selectedDistribution);
|
||||
}
|
||||
$nextStepUrl = $this->uriBuilder->buildUriFromRoute('login');
|
||||
|
||||
if ($siteSetup === 'createsite') {
|
||||
$siteUrl = $request->getAttribute('normalizedParams')->getSiteUrl();
|
||||
$this->setupService->createSite('main', $siteUrl);
|
||||
} elseif ($siteSetup === 'loaddistribution'
|
||||
&& !Environment::isComposerMode()
|
||||
&& $this->packageManager->isPackageActive('extensionmanager')
|
||||
) {
|
||||
// Update the URL to redirect after login to the extension manager distributions list
|
||||
$nextStepUrl = $this->uriBuilder->buildUriWithRedirect(
|
||||
'login',
|
||||
[],
|
||||
RouteRedirect::create(
|
||||
'extensionmanager',
|
||||
[
|
||||
'action' => 'distributions',
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (($request->getParsedBody()['install']['values']['backendgroups'] ?? '') === 'creategroups') {
|
||||
$this->setupService->createBackendUserGroups();
|
||||
}
|
||||
|
||||
$this->bootService->unsetInternalContainerInstance();
|
||||
$container = $this->bootService->loadExtLocalconfDatabase(true);
|
||||
|
||||
// Mark upgrade wizards as done
|
||||
$this->setupDatabaseService->markWizardsDone($container);
|
||||
|
||||
// Set up all installed extensions
|
||||
// (includes e.g. publishing of assets, importing distribution data)
|
||||
$this->setupService->setupExtensions($container);
|
||||
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$formProtection->clean();
|
||||
|
||||
EnableFileService::removeFirstInstallFile();
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'redirect' => (string)$nextStepUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to initialize a standalone view instance.
|
||||
*/
|
||||
private function initializeView(ServerRequestInterface $request): ViewInterface
|
||||
{
|
||||
$templatePaths = [
|
||||
'templateRootPaths' => ['EXT:install/Resources/Private/Templates'],
|
||||
];
|
||||
$renderingContext = $this->renderingContextFactory->create($templatePaths, $request);
|
||||
$fluidView = new FluidTemplateView($renderingContext);
|
||||
return new FluidViewAdapter($fluidView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Configuration\Exception\SettingsWriteException;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
||||
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
|
||||
use TYPO3\CMS\Core\Service\Exception\ConfigurationChangedException;
|
||||
use TYPO3\CMS\Core\Service\Exception\SilentConfigurationUpgradeReadonlyException;
|
||||
use TYPO3\CMS\Core\Service\SilentConfigurationUpgradeService;
|
||||
use TYPO3\CMS\Core\Type\Map;
|
||||
use TYPO3\CMS\Install\Factory\ImportMapFactory;
|
||||
use TYPO3\CMS\Install\Service\Exception\TemplateFileChangedException;
|
||||
use TYPO3\CMS\Install\Service\SilentTemplateFileUpgradeService;
|
||||
|
||||
/**
|
||||
* Layout controller
|
||||
*
|
||||
* Renders a first "load the Javascript in <head>" view, and the
|
||||
* main layout of the install tool in second action.
|
||||
*
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class LayoutController extends AbstractController
|
||||
{
|
||||
use ControllerTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly SilentConfigurationUpgradeService $silentConfigurationUpgradeService,
|
||||
private readonly SilentTemplateFileUpgradeService $silentTemplateFileUpgradeService,
|
||||
private readonly BackendEntryPointResolver $backendEntryPointResolver,
|
||||
private readonly ImportMapFactory $importMapFactory,
|
||||
private readonly HashService $hashService,
|
||||
private readonly IconRegistry $iconRegistry,
|
||||
private readonly DirectiveHashCollection $directiveHashCollection,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The init action renders an HTML response with HTML view having <head> section
|
||||
* containing resources to main .js routing.
|
||||
*/
|
||||
public function initAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$bust = $GLOBALS['EXEC_TIME'];
|
||||
if (!Environment::getContext()->isDevelopment()) {
|
||||
$bust = $this->hashService->hmac((new Typo3Version()) . Environment::getProjectPath(), self::class);
|
||||
}
|
||||
|
||||
$sitePath = $request->getAttribute('normalizedParams')->getSitePath();
|
||||
$importMap = $this->importMapFactory->create($sitePath);
|
||||
$initModule = $importMap->resolveImport('@typo3/install/init-install.js', true, $sitePath);
|
||||
|
||||
$view = $this->initializeView($request);
|
||||
$nonce = new ConsumableNonce();
|
||||
$view->assignMultiple([
|
||||
// time is used as cache bust for js and css resources
|
||||
'bust' => $bust,
|
||||
'iconCacheIdentifier' => sha1($this->iconRegistry->getBackendIconsCacheIdentifier()),
|
||||
'initModule' => $initModule,
|
||||
'importmap' => $importMap->render($sitePath, $nonce),
|
||||
]);
|
||||
return new HtmlResponse(
|
||||
$view->render('Layout/Init'),
|
||||
200,
|
||||
[
|
||||
'Cache-Control' => 'no-cache, no-store',
|
||||
'Content-Security-Policy' => $this->createContentSecurityPolicy()->compile(new PolicyBag(Scope::backend(), new Map(), new Behavior(), $nonce, $this->directiveHashCollection)),
|
||||
'Pragma' => 'no-cache',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a json response with the main HTML layout body: Toolbar, main menu and
|
||||
* doc header in standalone, doc header only in backend context. Silent updaters
|
||||
* are executed before this main view is loaded.
|
||||
*/
|
||||
public function mainLayoutAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$view->assign('moduleName', 'system_' . ($request->getQueryParams()['install']['module'] ?? 'layout'));
|
||||
$view->assign('backendUrl', (string)$this->backendEntryPointResolver->getUriFromRequest($request));
|
||||
$view->assign('frontendUrl', $request->getAttribute('normalizedParams')->getSiteUrl());
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Layout/MainLayout'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute silent configuration update. May be called multiple times until success = true is returned.
|
||||
*
|
||||
* @return ResponseInterface success = true if no change has been done
|
||||
*/
|
||||
public function executeSilentConfigurationUpdateAction(): ResponseInterface
|
||||
{
|
||||
$success = true;
|
||||
try {
|
||||
$this->silentConfigurationUpgradeService->execute();
|
||||
} catch (ConfigurationChangedException) {
|
||||
$success = false;
|
||||
} catch (SettingsWriteException $e) {
|
||||
throw new SilentConfigurationUpgradeReadonlyException(1688462974, $e);
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => $success,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute silent template files update. May be called multiple times until success = true is returned.
|
||||
*
|
||||
* @return ResponseInterface success = true if no change has been done
|
||||
*/
|
||||
public function executeSilentTemplateFileUpdateAction(): ResponseInterface
|
||||
{
|
||||
$success = true;
|
||||
try {
|
||||
$this->silentTemplateFileUpgradeService->execute();
|
||||
} catch (TemplateFileChangedException $e) {
|
||||
$success = false;
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => $success,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize TYPO3_CONF_VARS['EXTENSIONS'] with possibly new defaults from extensions
|
||||
* ext_conf_template.txt files. This make LocalConfiguration the only source of truth for
|
||||
* extension configuration, and it is always up-to-date, also if an extension has been
|
||||
* updated.
|
||||
*/
|
||||
public function executeSilentExtensionConfigurationSynchronizationAction(): ResponseInterface
|
||||
{
|
||||
$extensionConfiguration = new ExtensionConfiguration();
|
||||
$extensionConfiguration->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Install\Service\EnableFileService;
|
||||
|
||||
/**
|
||||
* Login controller
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class LoginController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FormProtectionFactory $formProtectionFactory,
|
||||
private readonly ConfigurationManager $configurationManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Render the "Create an "enable install tool file" action
|
||||
*/
|
||||
public function showEnableInstallToolFileAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$view->assign('enableInstallToolPath', EnableFileService::getStaticLocationForInstallToolEnableFileDirectory());
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Login/ShowEnableInstallToolFile'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render login view
|
||||
*/
|
||||
public function showLoginAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view = $this->initializeView($request);
|
||||
$view->assignMultiple([
|
||||
'loginToken' => $formProtection->generateToken('installTool', 'login'),
|
||||
'installToolEnableFilePermanent' => EnableFileService::isInstallToolEnableFilePermanent(),
|
||||
'configFile' => $this->configurationManager->getSystemConfigurationFileLocation(true),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Login/ShowLogin'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,891 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\ReferenceIndex;
|
||||
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
|
||||
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
|
||||
use TYPO3\CMS\Core\Database\Schema\SqlReader;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguagePackService;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
use TYPO3\CMS\Core\Localization\Locales;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
|
||||
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Install\Service\ClearCacheService;
|
||||
use TYPO3\CMS\Install\Service\ClearTableService;
|
||||
use TYPO3\CMS\Install\Service\LateBootService;
|
||||
use TYPO3\CMS\Install\Service\Typo3tempFileService;
|
||||
|
||||
/**
|
||||
* Maintenance controller
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class MaintenanceController extends AbstractController
|
||||
{
|
||||
protected PasswordPolicyValidator $passwordPolicyValidator;
|
||||
|
||||
public function __construct(
|
||||
private readonly LateBootService $lateBootService,
|
||||
private readonly ClearCacheService $clearCacheService,
|
||||
private readonly ConfigurationManager $configurationManager,
|
||||
private readonly PasswordHashFactory $passwordHashFactory,
|
||||
private readonly Locales $locales,
|
||||
private readonly LanguageServiceFactory $languageServiceFactory,
|
||||
private readonly FormProtectionFactory $formProtectionFactory,
|
||||
) {
|
||||
$GLOBALS['LANG'] = $this->languageServiceFactory->create('en');
|
||||
$passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default';
|
||||
$this->passwordPolicyValidator = GeneralUtility::makeInstance(
|
||||
PasswordPolicyValidator::class,
|
||||
PasswordPolicyAction::NEW_USER_PASSWORD,
|
||||
is_string($passwordPolicy) ? $passwordPolicy : ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main "show the cards" view
|
||||
*/
|
||||
public function cardsAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Maintenance/Cards'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache framework and opcode caches
|
||||
*/
|
||||
public function cacheClearAllAction(): ResponseInterface
|
||||
{
|
||||
$this->clearCacheService->clearAll();
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$messageQueue->enqueue(
|
||||
new FlashMessage('Successfully cleared all caches and all available opcode caches.', 'Caches cleared')
|
||||
);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear typo3temp files statistics action
|
||||
*/
|
||||
public function clearTypo3tempFilesStatsAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$container = $this->lateBootService->loadExtLocalconfDatabase(false);
|
||||
$typo3tempFileService = $container->get(Typo3tempFileService::class);
|
||||
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view->assignMultiple([
|
||||
'clearTypo3tempFilesToken' => $formProtection->generateToken('installTool', 'clearTypo3tempFiles'),
|
||||
]);
|
||||
return new JsonResponse(
|
||||
[
|
||||
'success' => true,
|
||||
'stats' => $typo3tempFileService->getDirectoryStatistics(),
|
||||
'html' => $view->render('Maintenance/ClearTypo3tempFiles'),
|
||||
'buttons' => [
|
||||
[
|
||||
'btnClass' => 'btn-default t3js-clearTypo3temp-stats',
|
||||
'text' => 'Scan again',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear typo3temp/assets or FAL processed Files
|
||||
*/
|
||||
public function clearTypo3tempFilesAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$container = $this->lateBootService->loadExtLocalconfDatabase(false);
|
||||
$typo3tempFileService = $container->get(Typo3tempFileService::class);
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$folder = $request->getParsedBody()['install']['folder'];
|
||||
// storageUid is an optional post param if FAL storages should be cleaned
|
||||
$storageUid = $request->getParsedBody()['install']['storageUid'] ?? null;
|
||||
if ($storageUid === null) {
|
||||
$typo3tempFileService->clearAssetsFolder($folder);
|
||||
$messageQueue->enqueue(new FlashMessage('The directory "' . $folder . '" has been cleared successfully', 'Directory cleared'));
|
||||
} else {
|
||||
$storageUid = (int)$storageUid;
|
||||
// We have to get the stats before deleting files, otherwise we're not able to retrieve the amount of files anymore
|
||||
$stats = $typo3tempFileService->getStatsFromStorageByUid($storageUid);
|
||||
$failedDeletions = $typo3tempFileService->clearProcessedFiles($storageUid);
|
||||
if ($failedDeletions) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Failed to delete ' . $failedDeletions . ' processed files. See TYPO3 log (by default typo3temp/var/log/typo3_*.log)',
|
||||
'Failed to delete files',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
sprintf('Removed %d files from directory "%s"', $stats['numberOfFiles'], $stats['directory']),
|
||||
'Deleted processed files'
|
||||
));
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump autoload information
|
||||
*/
|
||||
public function dumpAutoloadAction(): ResponseInterface
|
||||
{
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
if (Environment::isComposerMode()) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Skipped generating additional class loading information in Composer mode.',
|
||||
'Autoloader not dumped',
|
||||
ContextualFeedbackSeverity::NOTICE
|
||||
));
|
||||
} else {
|
||||
ClassLoadingInformation::dumpClassLoadingInformation();
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Successfully dumped class loading information for extensions.',
|
||||
'Dumped autoloader'
|
||||
));
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main database analyzer modal HTML
|
||||
*/
|
||||
public function databaseAnalyzerAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view->assignMultiple([
|
||||
'databaseAnalyzerExecuteToken' => $formProtection->generateToken('installTool', 'databaseAnalyzerExecute'),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Maintenance/DatabaseAnalyzer'),
|
||||
'buttons' => [
|
||||
[
|
||||
'btnClass' => 'btn-default t3js-databaseAnalyzer-analyze',
|
||||
'text' => 'Run database compare again',
|
||||
], [
|
||||
'btnClass' => 'btn-warning t3js-databaseAnalyzer-execute',
|
||||
'text' => 'Apply selected changes',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze current database situation
|
||||
*/
|
||||
public function databaseAnalyzerAnalyzeAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$container = $this->lateBootService->loadExtLocalconfDatabase();
|
||||
$schemaMigrator = $container->get(SchemaMigrator::class);
|
||||
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$suggestions = [];
|
||||
try {
|
||||
$sqlReader = $container->get(SqlReader::class);
|
||||
$sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
|
||||
$addCreateChange = $schemaMigrator->getUpdateSuggestions($sqlStatements);
|
||||
|
||||
// Aggregate the per-connection statements into one flat array
|
||||
$addCreateChange = array_merge_recursive(...array_values($addCreateChange));
|
||||
if (!empty($addCreateChange['create_table'])) {
|
||||
$suggestion = [
|
||||
'key' => 'addTable',
|
||||
'label' => 'Add tables',
|
||||
'enabled' => true,
|
||||
'children' => [],
|
||||
];
|
||||
foreach ($addCreateChange['create_table'] as $hash => $statement) {
|
||||
$suggestion['children'][] = [
|
||||
'hash' => $hash,
|
||||
'statement' => $statement,
|
||||
];
|
||||
}
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
if (!empty($addCreateChange['add'])) {
|
||||
$suggestion = [
|
||||
'key' => 'addField',
|
||||
'label' => 'Add fields to tables',
|
||||
'enabled' => true,
|
||||
'children' => [],
|
||||
];
|
||||
foreach ($addCreateChange['add'] as $hash => $statement) {
|
||||
$suggestion['children'][] = [
|
||||
'hash' => $hash,
|
||||
'statement' => $statement,
|
||||
];
|
||||
}
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
if (!empty($addCreateChange['change'])) {
|
||||
$suggestion = [
|
||||
'key' => 'change',
|
||||
'label' => 'Change fields',
|
||||
'enabled' => false,
|
||||
'children' => [],
|
||||
];
|
||||
foreach ($addCreateChange['change'] as $hash => $statement) {
|
||||
$child = [
|
||||
'hash' => $hash,
|
||||
'statement' => $statement,
|
||||
];
|
||||
if (isset($addCreateChange['change_currentValue'][$hash])) {
|
||||
$child['current'] = $addCreateChange['change_currentValue'][$hash];
|
||||
}
|
||||
$suggestion['children'][] = $child;
|
||||
}
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
|
||||
// Difference from current to expected
|
||||
$dropRename = $schemaMigrator->getUpdateSuggestions($sqlStatements, true);
|
||||
|
||||
// Aggregate the per-connection statements into one flat array
|
||||
$dropRename = array_merge_recursive(...array_values($dropRename));
|
||||
if (!empty($dropRename['change_table'])) {
|
||||
$suggestion = [
|
||||
'key' => 'renameTableToUnused',
|
||||
'label' => 'Remove tables (rename with prefix)',
|
||||
'enabled' => false,
|
||||
'children' => [],
|
||||
];
|
||||
foreach ($dropRename['change_table'] as $hash => $statement) {
|
||||
$child = [
|
||||
'hash' => $hash,
|
||||
'statement' => $statement,
|
||||
];
|
||||
if (!empty($dropRename['tables_count'][$hash])) {
|
||||
$child['rowCount'] = $dropRename['tables_count'][$hash];
|
||||
}
|
||||
$suggestion['children'][] = $child;
|
||||
}
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
if (!empty($dropRename['change'])) {
|
||||
$suggestion = [
|
||||
'key' => 'renameTableFieldToUnused',
|
||||
'label' => 'Remove unused fields (rename with prefix)',
|
||||
'enabled' => false,
|
||||
'children' => [],
|
||||
];
|
||||
foreach ($dropRename['change'] as $hash => $statement) {
|
||||
$suggestion['children'][] = [
|
||||
'hash' => $hash,
|
||||
'statement' => $statement,
|
||||
];
|
||||
}
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
if (!empty($dropRename['drop'])) {
|
||||
$suggestion = [
|
||||
'key' => 'deleteField',
|
||||
'label' => 'Drop fields (really!)',
|
||||
'enabled' => false,
|
||||
'children' => [],
|
||||
];
|
||||
foreach ($dropRename['drop'] as $hash => $statement) {
|
||||
$suggestion['children'][] = [
|
||||
'hash' => $hash,
|
||||
'statement' => $statement,
|
||||
];
|
||||
}
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
if (!empty($dropRename['drop_table'])) {
|
||||
$suggestion = [
|
||||
'key' => 'deleteTable',
|
||||
'label' => 'Drop tables (really!)',
|
||||
'enabled' => false,
|
||||
'children' => [],
|
||||
];
|
||||
foreach ($dropRename['drop_table'] as $hash => $statement) {
|
||||
$child = [
|
||||
'hash' => $hash,
|
||||
'statement' => $statement,
|
||||
];
|
||||
if (!empty($dropRename['tables_count'][$hash])) {
|
||||
$child['rowCount'] = $dropRename['tables_count'][$hash];
|
||||
}
|
||||
$suggestion['children'][] = $child;
|
||||
}
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
} catch (StatementException $e) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
$e->getMessage(),
|
||||
'Database analysis failed',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
'suggestions' => $suggestions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply selected database changes
|
||||
*/
|
||||
public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$container = $this->lateBootService->loadExtLocalconfDatabase();
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$selectedHashes = $request->getParsedBody()['install']['hashes'] ?? [];
|
||||
if (empty($selectedHashes)) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Please select any change by activating their respective checkboxes.',
|
||||
'No database changes selected',
|
||||
ContextualFeedbackSeverity::WARNING
|
||||
));
|
||||
} else {
|
||||
$sqlReader = $container->get(SqlReader::class);
|
||||
$sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
|
||||
$statementHashesToPerform = array_flip($selectedHashes);
|
||||
$schemaMigrator = $container->get(SchemaMigrator::class);
|
||||
$results = $schemaMigrator->migrate($sqlStatements, $statementHashesToPerform);
|
||||
// Create error flash messages if any
|
||||
foreach ($results as $errorMessage) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Error: ' . $errorMessage,
|
||||
'Database update failed',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
}
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Executed database updates',
|
||||
'Executed database updates'
|
||||
));
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear table overview statistics action
|
||||
*/
|
||||
public function clearTablesStatsAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view->assignMultiple([
|
||||
'clearTablesClearToken' => $formProtection->generateToken('installTool', 'clearTablesClear'),
|
||||
]);
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$clearTableService = $container->get(ClearTableService::class);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'stats' => $clearTableService->getTableStatistics(),
|
||||
'html' => $view->render('Maintenance/ClearTables'),
|
||||
'buttons' => [
|
||||
[
|
||||
'btnClass' => 'btn-default t3js-clearTables-stats',
|
||||
'text' => 'Scan again',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a specific table
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function clearTablesClearAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$table = $request->getParsedBody()['install']['table'];
|
||||
if (empty($table)) {
|
||||
throw new \RuntimeException(
|
||||
'No table name given',
|
||||
1501944076
|
||||
);
|
||||
}
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$clearTableService = $container->get(ClearTableService::class);
|
||||
$clearTableService->clearSelectedTable($table);
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$messageQueue->enqueue(
|
||||
new FlashMessage('The table ' . $table . ' has been cleared.', 'Table cleared')
|
||||
);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Create Admin Get Data action
|
||||
*/
|
||||
public function createAdminGetDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view->assignMultiple([
|
||||
'createAdminToken' => $formProtection->generateToken('installTool', 'createAdmin'),
|
||||
'passwordPolicyRequirements' => $this->passwordPolicyValidator->getRequirements(),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Maintenance/CreateAdmin'),
|
||||
'buttons' => [
|
||||
[
|
||||
'btnClass' => 'btn-default t3js-createAdmin-create',
|
||||
'text' => 'Create administrator user',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backend administrator from given username and password
|
||||
*/
|
||||
public function createAdminAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$userCreated = false;
|
||||
$username = preg_replace('/\\s/i', '', $request->getParsedBody()['install']['userName']);
|
||||
$password = $request->getParsedBody()['install']['userPassword'];
|
||||
$passwordCheck = $request->getParsedBody()['install']['userPasswordCheck'];
|
||||
$email = $request->getParsedBody()['install']['userEmail'] ?? '';
|
||||
$realName = $request->getParsedBody()['install']['realName'] ?? '';
|
||||
$isSystemMaintainer = ((bool)$request->getParsedBody()['install']['userSystemMaintainer'] == '1') ? true : false;
|
||||
|
||||
$messages = new FlashMessageQueue('install');
|
||||
$contextData = new ContextData(newUsername: $username);
|
||||
|
||||
if ($username === '') {
|
||||
$messages->enqueue(new FlashMessage(
|
||||
'No username given.',
|
||||
'Administrator user not created',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} elseif ($password !== $passwordCheck) {
|
||||
$messages->enqueue(new FlashMessage(
|
||||
'Passwords do not match.',
|
||||
'Administrator user not created',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} elseif (!$this->passwordPolicyValidator->isValidPassword($password, $contextData)) {
|
||||
$messages->enqueue(new FlashMessage(
|
||||
'The password does not meet the password policy requirements.',
|
||||
'Administrator user not created',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$connectionPool = $container->get(ConnectionPool::class);
|
||||
$userExists = $connectionPool->getConnectionForTable('be_users')
|
||||
->count(
|
||||
'uid',
|
||||
'be_users',
|
||||
['username' => $username]
|
||||
);
|
||||
if ($userExists) {
|
||||
$messages->enqueue(new FlashMessage(
|
||||
'A user with username "' . $username . '" exists already.',
|
||||
'Administrator user not created',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$hashInstance = $this->passwordHashFactory->getDefaultHashInstance('BE');
|
||||
$hashedPassword = $hashInstance->getHashedPassword($password);
|
||||
$adminUserFields = [
|
||||
'username' => $username,
|
||||
'password' => $hashedPassword,
|
||||
'admin' => 1,
|
||||
'realName' => $realName,
|
||||
'tstamp' => $GLOBALS['EXEC_TIME'],
|
||||
'crdate' => $GLOBALS['EXEC_TIME'],
|
||||
];
|
||||
if (GeneralUtility::validEmail($email)) {
|
||||
$adminUserFields['email'] = $email;
|
||||
}
|
||||
$connectionPool->getConnectionForTable('be_users')->insert('be_users', $adminUserFields);
|
||||
$userCreated = true;
|
||||
|
||||
if ($isSystemMaintainer) {
|
||||
// Get the new admin user uid just created
|
||||
$newAdminUserUid = (int)$connectionPool->getConnectionForTable('be_users')->lastInsertId();
|
||||
|
||||
// Get the list of the existing systemMaintainer
|
||||
$existingSystemMaintainersList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
|
||||
|
||||
// Add the new admin user to the existing systemMaintainer list
|
||||
$newSystemMaintainersList = $existingSystemMaintainersList;
|
||||
$newSystemMaintainersList[] = $newAdminUserUid;
|
||||
|
||||
// Update the system/settings.php file with the new list
|
||||
$this->configurationManager->setLocalConfigurationValuesByPathValuePairs(
|
||||
['SYS/systemMaintainers' => $newSystemMaintainersList]
|
||||
);
|
||||
}
|
||||
|
||||
$messages->enqueue(new FlashMessage(
|
||||
'An administrator with username "' . $username . '" has been created successfully.',
|
||||
'Administrator created'
|
||||
));
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messages,
|
||||
'userCreated' => $userCreated,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry action of language packs module gets
|
||||
* * list of available languages with details like active or not and last update
|
||||
* * list of loaded extensions
|
||||
*/
|
||||
public function languagePacksGetDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||
$view->assignMultiple([
|
||||
'isWritable' => $isWritable,
|
||||
'languagePacksActivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksActivateLanguage'),
|
||||
'languagePacksDeactivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksDeactivateLanguage'),
|
||||
'languagePacksUpdatePackToken' => $formProtection->generateToken('installTool', 'languagePacksUpdatePack'),
|
||||
'languagePacksUpdateIsoTimesToken' => $formProtection->generateToken('installTool', 'languagePacksUpdateIsoTimes'),
|
||||
]);
|
||||
// This action needs TYPO3_CONF_VARS for full GeneralUtility::getUrl() config
|
||||
$container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
|
||||
$languagePackService = $container->get(LanguagePackService::class);
|
||||
$extensions = $languagePackService->getExtensionLanguagePackDetails();
|
||||
$extensionList = array_map(function (array $extension) {
|
||||
$extension['packs'] = array_values($extension['packs']);
|
||||
return $extension;
|
||||
}, array_values($extensions));
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'languages' => $languagePackService->getLanguageDetails(),
|
||||
'extensions' => $extensionList,
|
||||
'activeLanguages' => $languagePackService->getActiveLanguages(),
|
||||
'activeExtensions' => array_column($extensions, 'key'),
|
||||
'html' => $view->render('Maintenance/LanguagePacks'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a language and any possible dependency it may have
|
||||
*/
|
||||
public function languagePacksActivateLanguageAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$languagePackService = $container->get(LanguagePackService::class);
|
||||
$availableLanguages = $languagePackService->getAvailableLanguages();
|
||||
$activeLanguages = $languagePackService->getActiveLanguages();
|
||||
$iso = $request->getParsedBody()['install']['iso'];
|
||||
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
sprintf('The language %s was not activated as the configuration file is not writable.', $availableLanguages[$iso]),
|
||||
'Language not activated',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$activateArray = [];
|
||||
foreach ($availableLanguages as $availableIso => $name) {
|
||||
if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) {
|
||||
$activateArray[] = $iso;
|
||||
$dependencies = $this->locales->getLocaleDependencies($availableIso);
|
||||
if (!empty($dependencies)) {
|
||||
foreach ($dependencies as $dependency) {
|
||||
if (!in_array($dependency, $activeLanguages, true)) {
|
||||
$activateArray[] = $dependency;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($activateArray)) {
|
||||
$activeLanguages = array_merge($activeLanguages, $activateArray);
|
||||
sort($activeLanguages);
|
||||
$this->configurationManager->setLocalConfigurationValueByPath(
|
||||
'LANG',
|
||||
['availableLocales' => $activeLanguages]
|
||||
);
|
||||
$activationArray = [];
|
||||
foreach ($activateArray as $activateIso) {
|
||||
$activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')';
|
||||
}
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'These languages have been activated: ' . implode(', ', $activationArray)
|
||||
));
|
||||
} else {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Language with ISO code "' . $iso . '" not found or already active.',
|
||||
'',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a language if no other active language depends on it
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function languagePacksDeactivateLanguageAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$languagePackService = $container->get(LanguagePackService::class);
|
||||
$availableLanguages = $languagePackService->getAvailableLanguages();
|
||||
$activeLanguages = $languagePackService->getActiveLanguages();
|
||||
$iso = $request->getParsedBody()['install']['iso'];
|
||||
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
sprintf('The language %s was not deactivated as the configuration file is not writable.', $availableLanguages[$iso]),
|
||||
'Language not deactivated',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
if (empty($iso)) {
|
||||
throw new \RuntimeException('No iso code given', 1520109807);
|
||||
}
|
||||
$otherActiveLanguageDependencies = [];
|
||||
foreach ($activeLanguages as $activeLanguage) {
|
||||
if ($activeLanguage === $iso) {
|
||||
continue;
|
||||
}
|
||||
$dependencies = $this->locales->getLocaleDependencies($activeLanguage);
|
||||
if (in_array($iso, $dependencies, true)) {
|
||||
$otherActiveLanguageDependencies[] = $activeLanguage;
|
||||
}
|
||||
}
|
||||
if (!empty($otherActiveLanguageDependencies)) {
|
||||
// Error: Must disable dependencies first
|
||||
$dependentArray = [];
|
||||
foreach ($otherActiveLanguageDependencies as $dependency) {
|
||||
$dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')';
|
||||
}
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These'
|
||||
. ' other languages depend on it and need to be deactivated before:'
|
||||
. implode(', ', $dependentArray),
|
||||
'',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
if (in_array($iso, $activeLanguages, true)) {
|
||||
// Deactivate this language
|
||||
$newActiveLanguages = [];
|
||||
foreach ($activeLanguages as $activeLanguage) {
|
||||
if ($activeLanguage === $iso) {
|
||||
continue;
|
||||
}
|
||||
$newActiveLanguages[] = $activeLanguage;
|
||||
}
|
||||
$this->configurationManager->setLocalConfigurationValueByPath(
|
||||
'LANG',
|
||||
['availableLocales' => $newActiveLanguages]
|
||||
);
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated'
|
||||
));
|
||||
} else {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated',
|
||||
'',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a pack of one extension and one language
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function languagePacksUpdatePackAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
|
||||
$iso = $request->getParsedBody()['install']['iso'];
|
||||
$key = $request->getParsedBody()['install']['extension'];
|
||||
|
||||
$languagePackService = $container->get(LanguagePackService::class);
|
||||
|
||||
// Gate untrusted user input against the set of extensions and languages exposed for download.
|
||||
$extensions = $languagePackService->getExtensionLanguagePackDetails();
|
||||
if (!isset($extensions[$key]['packs'][$iso])) {
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'packResult' => 'skipped',
|
||||
]);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'packResult' => $languagePackService->languagePackDownload($key, $iso),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set "last updated" time in registry for fully updated language packs.
|
||||
*/
|
||||
public function languagePacksUpdateIsoTimesAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$isos = $request->getParsedBody()['install']['isos'];
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$languagePackService = $container->get(LanguagePackService::class);
|
||||
$languagePackService->setLastUpdatedIsoCode($isos);
|
||||
|
||||
// The cache manager is already instantiated in the install tool
|
||||
// with some hacked settings to disable caching of extbase and fluid.
|
||||
// We want a "fresh" object here to operate on a different cache setup.
|
||||
// cacheManager implements SingletonInterface, so the only way to get a "fresh"
|
||||
// instance is by circumventing makeInstance and using new directly!
|
||||
$cacheManager = new CacheManager();
|
||||
$cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
|
||||
$cacheManager->getCache('l10n')->flush();
|
||||
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 'uc' field of all backend users to empty string
|
||||
*/
|
||||
public function resetBackendUserUcAction(): ResponseInterface
|
||||
{
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$connectionPool = $container->get(ConnectionPool::class);
|
||||
$connectionPool
|
||||
->getQueryBuilderForTable('be_users')
|
||||
->update('be_users')
|
||||
->set('uc', '')
|
||||
->executeStatement();
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Preferences of all backend users have been reset',
|
||||
'Reset preferences of all backend users'
|
||||
));
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show reference index card
|
||||
*/
|
||||
public function referenceIndexAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$view->assignMultiple([
|
||||
'referenceIndexToken' => $this->formProtectionFactory->createFromRequest($request)->generateToken('installTool', 'referenceIndexUpdate'),
|
||||
'binaryPath' => ExtensionManagementUtility::extPath('core', 'bin/typo3'),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Maintenance/ReferenceIndex'),
|
||||
'buttons' => [
|
||||
[
|
||||
'btnClass' => 'btn-default t3js-referenceIndex-check',
|
||||
'text' => 'Check Reference Index',
|
||||
],
|
||||
[
|
||||
'btnClass' => 'btn-default t3js-referenceIndex-update',
|
||||
'text' => 'Update Reference Index',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check or update reference index
|
||||
*/
|
||||
public function referenceIndexUpdateAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$isCheckOnly = (bool)($request->getParsedBody()['install']['checkOnly'] ?? false);
|
||||
|
||||
$container = $this->lateBootService->loadExtLocalconfDatabase(false, true);
|
||||
$result = $container->get(ReferenceIndex::class)->updateIndex($isCheckOnly);
|
||||
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
if (!empty($result['errors'])) {
|
||||
foreach ($result['errors'] as $error) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
$error,
|
||||
'Reference Index Issue',
|
||||
ContextualFeedbackSeverity::WARNING
|
||||
));
|
||||
}
|
||||
} else {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
$isCheckOnly ? 'Reference index check completed successfully' : 'Reference index has been updated successfully',
|
||||
$isCheckOnly ? 'Check Complete' : 'Update Complete'
|
||||
));
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Used from backend `/typo3` context to check webserver response in general (independent of install tool).
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
readonly class ServerResponseCheckController
|
||||
{
|
||||
public function __construct(private HashService $hashService) {}
|
||||
|
||||
public function checkHostAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$time = $request->getQueryParams()['src-time'] ?? null;
|
||||
$hash = $request->getQueryParams()['src-hash'] ?? null;
|
||||
|
||||
if (empty($time) || !is_string($time) || empty($hash) || !is_string($hash)) {
|
||||
return new JsonResponse(['error' => 'Query params src-time` and src-hash` are required.'], 400);
|
||||
}
|
||||
|
||||
$expectedHash = $this->hashService->hmac($time, 'server-response-check');
|
||||
if (!hash_equals($expectedHash, $hash)) {
|
||||
return new JsonResponse(['error' => 'Invalid time or hash provided.'], 400);
|
||||
}
|
||||
if ((int)$time + 60 < time()) {
|
||||
return new JsonResponse(['error' => 'Request expired.'], 400);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'server.HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? null,
|
||||
'server.SERVER_NAME' => $_SERVER['SERVER_NAME'] ?? null,
|
||||
'server.SERVER_PORT' => $_SERVER['SERVER_PORT'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
<?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\Install\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
use TYPO3\CMS\Core\Localization\Locales;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\PasswordService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CommentAwareAstBuilder;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstConstantCommentVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Install\Configuration\FeatureManager;
|
||||
use TYPO3\CMS\Install\Service\LateBootService;
|
||||
use TYPO3\CMS\Install\Service\LocalConfigurationValueService;
|
||||
|
||||
/**
|
||||
* Settings controller
|
||||
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SettingsController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LateBootService $lateBootService,
|
||||
private readonly PackageManager $packageManager,
|
||||
private readonly LanguageServiceFactory $languageServiceFactory,
|
||||
private readonly CommentAwareAstBuilder $astBuilder,
|
||||
private readonly LosslessTokenizer $losslessTokenizer,
|
||||
private readonly AstTraverser $astTraverser,
|
||||
private readonly FormProtectionFactory $formProtectionFactory,
|
||||
private readonly ConfigurationManager $configurationManager,
|
||||
private readonly PasswordService $passwordService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Main "show the cards" view
|
||||
*/
|
||||
public function cardsAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$view->assign('isWritable', $this->configurationManager->canWriteConfiguration());
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Settings/Cards'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change install tool password
|
||||
*/
|
||||
public function changeInstallToolPasswordGetDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||
$view->assignMultiple([
|
||||
'isWritable' => $isWritable,
|
||||
'changeInstallToolPasswordToken' => $formProtection->generateToken('installTool', 'changeInstallToolPassword'),
|
||||
]);
|
||||
$buttons = [];
|
||||
if ($isWritable) {
|
||||
$buttons[] = [
|
||||
'btnClass' => 'btn-default t3js-changeInstallToolPassword-change',
|
||||
'text' => 'Set new password',
|
||||
];
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Settings/ChangeInstallToolPassword'),
|
||||
'buttons' => $buttons,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change install tool password
|
||||
*/
|
||||
public function changeInstallToolPasswordAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'The configuration file is not writable.',
|
||||
'Configuration not writable',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$password = $request->getParsedBody()['install']['password'] ?? '';
|
||||
$passwordCheck = $request->getParsedBody()['install']['passwordCheck'];
|
||||
$validationResultErrors = $this->passwordService->getValidationErrorsForInstallToolUpdate($password);
|
||||
if ($password !== $passwordCheck) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'Given passwords do not match.',
|
||||
'Install tool password not changed',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} elseif ($validationResultErrors !== []) {
|
||||
$errors = array_values($validationResultErrors);
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
implode('. ', $errors) . '.',
|
||||
'Install tool password not changed',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
|
||||
$this->configurationManager->setLocalConfigurationValueByPath(
|
||||
'BE/installToolPassword',
|
||||
$hashInstance->getHashedPassword($password)
|
||||
);
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'The Install tool password has been changed successfully.',
|
||||
'Install tool password changed'
|
||||
));
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of possible and active system maintainers
|
||||
*/
|
||||
public function systemMaintainerGetListAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$connectionPool = $container->get(ConnectionPool::class);
|
||||
|
||||
// We have to respect the enable fields here by our own because no TCA is loaded
|
||||
$queryBuilder = $connectionPool->getQueryBuilderForTable('be_users');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$users = $queryBuilder
|
||||
->select('uid', 'username', 'disable', 'starttime', 'endtime')
|
||||
->from('be_users')
|
||||
->where(
|
||||
$queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('_cli_'))
|
||||
)
|
||||
)
|
||||
->orderBy('uid')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$systemMaintainerList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
|
||||
$systemMaintainerList = array_map('intval', $systemMaintainerList);
|
||||
$currentTime = time();
|
||||
foreach ($users as &$user) {
|
||||
$user['disable'] = $user['disable']
|
||||
|| ((int)$user['starttime'] !== 0 && $user['starttime'] > $currentTime)
|
||||
|| ((int)$user['endtime'] !== 0 && $user['endtime'] < $currentTime);
|
||||
$user['isSystemMaintainer'] = in_array((int)$user['uid'], $systemMaintainerList, true);
|
||||
}
|
||||
$view = $this->initializeView($request);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||
$view->assignMultiple([
|
||||
'isWritable' => $isWritable,
|
||||
'users' => $users,
|
||||
'systemMaintainerWriteToken' => $formProtection->generateToken('installTool', 'systemMaintainerWrite'),
|
||||
'systemMaintainerIsDevelopmentContext' => Environment::getContext()->isDevelopment(),
|
||||
]);
|
||||
$buttons = [];
|
||||
if ($isWritable) {
|
||||
$buttons[] = [
|
||||
'btnClass' => 'btn-default t3js-systemMaintainer-write',
|
||||
'text' => 'Save system maintainer list',
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'users' => $users,
|
||||
'html' => $view->render('Settings/SystemMaintainer'),
|
||||
'buttons' => $buttons,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write new system maintainer list
|
||||
*/
|
||||
public function systemMaintainerWriteAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$messages = [];
|
||||
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$messages[] = new FlashMessage(
|
||||
'The configuration file is not writable.',
|
||||
'Configuration not writable',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
}
|
||||
// Sanitize given user list and write out
|
||||
$newUserList = [];
|
||||
$users = $request->getParsedBody()['install']['users'] ?? [];
|
||||
if (is_array($users)) {
|
||||
foreach ($users as $uid) {
|
||||
if (MathUtility::canBeInterpretedAsInteger($uid)) {
|
||||
$newUserList[] = (int)$uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$container = $this->lateBootService->getContainer(true);
|
||||
$connectionPool = $container->get(ConnectionPool::class);
|
||||
$queryBuilder = $connectionPool->getQueryBuilderForTable('be_users');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
|
||||
$validatedUserList = $queryBuilder
|
||||
->select('uid')
|
||||
->from('be_users')
|
||||
->where(
|
||||
$queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($newUserList, Connection::PARAM_INT_ARRAY))
|
||||
)
|
||||
)->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$validatedUserList = array_column($validatedUserList, 'uid');
|
||||
$validatedUserList = array_map('intval', $validatedUserList);
|
||||
|
||||
$this->configurationManager->setLocalConfigurationValuesByPathValuePairs(
|
||||
['SYS/systemMaintainers' => $validatedUserList]
|
||||
);
|
||||
|
||||
if (empty($validatedUserList)) {
|
||||
$messages[] = new FlashMessage(
|
||||
'The system has no maintainers enabled anymore. Please use the standalone Install Tools from now on.',
|
||||
'Cleared system maintainer list',
|
||||
ContextualFeedbackSeverity::INFO
|
||||
);
|
||||
} else {
|
||||
$messages[] = new FlashMessage(
|
||||
'New system maintainer uid list: ' . implode(', ', $validatedUserList),
|
||||
'Updated system maintainers',
|
||||
ContextualFeedbackSeverity::INFO
|
||||
);
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main LocalConfiguration card content
|
||||
*/
|
||||
public function localConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$localConfigurationValueService = new LocalConfigurationValueService();
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||
$view = $this->initializeView($request);
|
||||
$view->assignMultiple([
|
||||
'isWritable' => $isWritable,
|
||||
'localConfigurationWriteToken' => $formProtection->generateToken('installTool', 'localConfigurationWrite'),
|
||||
'localConfigurationData' => $this->enrichConfigurationData($localConfigurationValueService->getCurrentConfigurationData()),
|
||||
]);
|
||||
|
||||
$buttons = [
|
||||
[
|
||||
'btnClass' => 'btn-default t3js-localConfiguration-toggleAll',
|
||||
'text' => 'Toggle All',
|
||||
],
|
||||
];
|
||||
|
||||
if ($isWritable) {
|
||||
$buttons[] = [
|
||||
'btnClass' => 'btn-default t3js-localConfiguration-write',
|
||||
'text' => 'Write configuration',
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Settings/LocalConfigurationGetContent'),
|
||||
'buttons' => $buttons,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write given LocalConfiguration settings
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function localConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'The configuration file is not writable.',
|
||||
'Configuration not writable',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$settings = $request->getParsedBody()['install']['configurationValues'];
|
||||
if (!is_array($settings) || empty($settings)) {
|
||||
throw new \RuntimeException(
|
||||
'Expected value array not found',
|
||||
1502282283
|
||||
);
|
||||
}
|
||||
$localConfigurationValueService = new LocalConfigurationValueService();
|
||||
$messageQueue = $localConfigurationValueService->updateLocalConfigurationValues($settings);
|
||||
if ($messageQueue->count() === 0) {
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
'No configuration changes have been detected in the submitted form.',
|
||||
'Configuration not updated',
|
||||
ContextualFeedbackSeverity::WARNING
|
||||
));
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messageQueue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main preset card content
|
||||
*/
|
||||
public function presetsGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->initializeView($request);
|
||||
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||
$presetFeatures = GeneralUtility::makeInstance(FeatureManager::class);
|
||||
$presetFeatures = $presetFeatures->getInitializedFeatures($request->getParsedBody()['install']['values'] ?? []);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view->assignMultiple([
|
||||
'isWritable' => $isWritable,
|
||||
'presetsActivateToken' => $formProtection->generateToken('installTool', 'presetsActivate'),
|
||||
// This action is called again from within the card itself if a custom image path is supplied
|
||||
'presetsGetContentToken' => $formProtection->generateToken('installTool', 'presetsGetContent'),
|
||||
'presetFeatures' => $presetFeatures,
|
||||
]);
|
||||
$buttons = [];
|
||||
if ($isWritable) {
|
||||
$buttons[] = [
|
||||
'btnClass' => 'btn-default t3js-presets-activate',
|
||||
'text' => 'Activate preset',
|
||||
];
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Settings/PresetsGetContent'),
|
||||
'buttons' => $buttons,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write selected presets
|
||||
*/
|
||||
public function presetsActivateAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$messages = new FlashMessageQueue('install');
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$messages->enqueue(new FlashMessage(
|
||||
'The configuration file is not writable.',
|
||||
'Configuration not writable',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
));
|
||||
} else {
|
||||
$featureManager = new FeatureManager();
|
||||
$configurationValues = $featureManager->getConfigurationForSelectedFeaturePresets($request->getParsedBody()['install']['values'] ?? []);
|
||||
if (!empty($configurationValues)) {
|
||||
$this->configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationValues);
|
||||
$messageBody = [];
|
||||
foreach ($configurationValues as $configurationKey => $configurationValue) {
|
||||
if (is_array($configurationValue)) {
|
||||
$configurationValue = json_encode($configurationValue);
|
||||
}
|
||||
$messageBody[] = '\'' . $configurationKey . '\' => \'' . $configurationValue . '\'';
|
||||
}
|
||||
$messages->enqueue(new FlashMessage(
|
||||
implode(', ', $messageBody),
|
||||
'Configuration written'
|
||||
));
|
||||
} else {
|
||||
$messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'No configuration change selected',
|
||||
ContextualFeedbackSeverity::INFO
|
||||
));
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of extensions with their configuration form.
|
||||
*/
|
||||
public function extensionConfigurationGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Extension configuration needs initialized $GLOBALS['LANG']
|
||||
$GLOBALS['LANG'] = $this->languageServiceFactory->create('en');
|
||||
$extensionsWithConfigurations = [];
|
||||
$activePackages = $this->packageManager->getActivePackages();
|
||||
$extensionConfiguration = new ExtensionConfiguration();
|
||||
foreach ($activePackages as $extensionKey => $activePackage) {
|
||||
if (@file_exists($activePackage->getPackagePath() . 'ext_conf_template.txt')) {
|
||||
$ast = $this->astBuilder->build(
|
||||
$this->losslessTokenizer->tokenize(file_get_contents($activePackage->getPackagePath() . 'ext_conf_template.txt')),
|
||||
new RootNode()
|
||||
);
|
||||
$astConstantCommentVisitor = new (AstConstantCommentVisitor::class);
|
||||
$this->astTraverser->traverse($ast, [$astConstantCommentVisitor]);
|
||||
$constants = $astConstantCommentVisitor->getConstants();
|
||||
// @todo: It would be better to fetch all LocalConfiguration settings of an extension at once
|
||||
// and feed it as pseudo-TS to the AST builder. This way the full AstConstantCommentVisitor
|
||||
// preparation magic would kick in and the JS-side processing in extension-configuration.ts
|
||||
// could be removed (especially the 'wrap' and 'offset' stuff) by handling it in fluid directly.
|
||||
foreach ($constants as $constantName => &$constantDetails) {
|
||||
try {
|
||||
$valueFromLocalConfiguration = $extensionConfiguration->get($extensionKey, str_replace('.', '/', $constantName));
|
||||
$constantDetails['value'] = $valueFromLocalConfiguration;
|
||||
} catch (ExtensionConfigurationPathDoesNotExistException $e) {
|
||||
// Deliberately empty - it can happen at runtime that a written config does not return
|
||||
// back all values (eg. saltedpassword with its userFuncs), which then miss in the written
|
||||
// configuration and are only synced after next install tool run. This edge case is
|
||||
// taken care of here.
|
||||
}
|
||||
}
|
||||
$displayConstants = [];
|
||||
foreach ($astConstantCommentVisitor->getCategories() as $category => $details) {
|
||||
if ($details['usageCount'] > 0) {
|
||||
$displayConstants[$category]['label'] = $details['label'];
|
||||
}
|
||||
}
|
||||
foreach ($constants as $constant) {
|
||||
$displayConstants[$constant['cat']]['items'][$constant['subcat_sorting_first']]['label'] = $constant['subcat_label'];
|
||||
$displayConstants[$constant['cat']]['items'][$constant['subcat_sorting_first']]['items'][$constant['subcat_sorting_second']] = $constant;
|
||||
}
|
||||
foreach ($displayConstants as &$constantCategory) {
|
||||
ksort($constantCategory['items'], SORT_NATURAL);
|
||||
foreach ($constantCategory['items'] as &$constantDetailItems) {
|
||||
ksort($constantDetailItems['items'], SORT_NATURAL);
|
||||
}
|
||||
}
|
||||
$extensionsWithConfigurations[$extensionKey] = $displayConstants;
|
||||
}
|
||||
}
|
||||
ksort($extensionsWithConfigurations);
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||
$view = $this->initializeView($request);
|
||||
$view->assignMultiple([
|
||||
'isWritable' => $isWritable,
|
||||
'extensionsWithConfigurations' => $extensionsWithConfigurations,
|
||||
'extensionConfigurationWriteToken' => $formProtection->generateToken('installTool', 'extensionConfigurationWrite'),
|
||||
]);
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Settings/ExtensionConfigurationGetContent'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write extension configuration
|
||||
*/
|
||||
public function extensionConfigurationWriteAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$messages = [];
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$messages[] = new FlashMessage(
|
||||
'The configuration file is not writable.',
|
||||
'Configuration not writable',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
} else {
|
||||
$extensionKey = $request->getParsedBody()['install']['extensionKey'];
|
||||
$configuration = $request->getParsedBody()['install']['extensionConfiguration'] ?? [];
|
||||
$nestedConfiguration = [];
|
||||
foreach ($configuration as $configKey => $value) {
|
||||
$nestedConfiguration = ArrayUtility::setValueByPath($nestedConfiguration, $configKey, $value, '.');
|
||||
}
|
||||
(new ExtensionConfiguration())->set($extensionKey, $nestedConfiguration);
|
||||
$messages[] = new FlashMessage(
|
||||
'Successfully saved configuration for extension "' . $extensionKey . '".',
|
||||
'Configuration saved',
|
||||
ContextualFeedbackSeverity::OK
|
||||
);
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => $messages,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render feature toggles
|
||||
*/
|
||||
public function featuresGetContentAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$isWritable = $this->configurationManager->canWriteConfiguration();
|
||||
$configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
|
||||
->load($this->configurationManager->getDefaultConfigurationDescriptionFileLocation());
|
||||
$allFeatures = $GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? [];
|
||||
$features = [];
|
||||
foreach ($allFeatures as $featureName => $featureValue) {
|
||||
// Only features that have a .yml description will be listed. There is currently no
|
||||
// way for extensions to extend this, so feature toggles of non-core extensions are
|
||||
// not listed here.
|
||||
if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
|
||||
$default = $this->configurationManager->getDefaultConfigurationValueByPath('SYS/features/' . $featureName);
|
||||
$features[] = [
|
||||
'label' => ucfirst(str_replace(['_', '.'], ' ', strtolower(GeneralUtility::camelCaseToLowerCaseUnderscored(preg_replace('/\./', ': ', $featureName, 1))))),
|
||||
'name' => $featureName,
|
||||
'description' => $configurationDescription['SYS']['items']['features']['items'][$featureName]['description'],
|
||||
'default' => $default,
|
||||
'value' => $featureValue,
|
||||
];
|
||||
}
|
||||
}
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
$view = $this->initializeView($request);
|
||||
$view->assignMultiple([
|
||||
'isWritable' => $isWritable,
|
||||
'features' => $features,
|
||||
'featuresSaveToken' => $formProtection->generateToken('installTool', 'featuresSave'),
|
||||
]);
|
||||
$buttons = [];
|
||||
if ($isWritable) {
|
||||
$buttons[] = [
|
||||
'btnClass' => 'btn-default t3js-features-save',
|
||||
'text' => 'Save',
|
||||
];
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'html' => $view->render('Settings/FeaturesGetContent'),
|
||||
'buttons' => $buttons,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update feature toggles state
|
||||
*/
|
||||
public function featuresSaveAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->configurationManager->canWriteConfiguration()) {
|
||||
$message = new FlashMessage(
|
||||
'The configuration file is not writable.',
|
||||
'Configuration not writable',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
} else {
|
||||
$enabledFeaturesFromPost = $request->getParsedBody()['install']['values'] ?? [];
|
||||
$allFeatures = array_keys($GLOBALS['TYPO3_CONF_VARS']['SYS']['features'] ?? []);
|
||||
$configurationDescription = GeneralUtility::makeInstance(YamlFileLoader::class)
|
||||
->load($this->configurationManager->getDefaultConfigurationDescriptionFileLocation());
|
||||
$updatedFeatures = [];
|
||||
$configurationPathValuePairs = [];
|
||||
foreach ($allFeatures as $featureName) {
|
||||
// Only features that have a .yml description will be listed. There is currently no
|
||||
// way for extensions to extend this, so feature toggles of non-core extensions are
|
||||
// not considered.
|
||||
if (isset($configurationDescription['SYS']['items']['features']['items'][$featureName]['description'])) {
|
||||
$path = 'SYS/features/' . $featureName;
|
||||
$newValue = isset($enabledFeaturesFromPost[$featureName]);
|
||||
if ($newValue !== $this->configurationManager->getConfigurationValueByPath($path)) {
|
||||
$configurationPathValuePairs[$path] = $newValue;
|
||||
$updatedFeatures[] = $featureName . ' [' . ($newValue ? 'On' : 'Off') . ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($configurationPathValuePairs !== []) {
|
||||
$success = $this->configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationPathValuePairs);
|
||||
if ($success) {
|
||||
$this->configurationManager->exportConfiguration();
|
||||
$message = new FlashMessage(
|
||||
"Successfully updated the following feature toggles:\n" . implode(",\n", $updatedFeatures),
|
||||
'Features updated',
|
||||
ContextualFeedbackSeverity::OK
|
||||
);
|
||||
} else {
|
||||
$message = new FlashMessage(
|
||||
'An error occurred while saving. Some settings may not have been updated.',
|
||||
'Features not updated',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$message = new FlashMessage(
|
||||
'Nothing to update.',
|
||||
'Features not updated',
|
||||
ContextualFeedbackSeverity::INFO
|
||||
);
|
||||
}
|
||||
}
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'status' => [$message],
|
||||
]);
|
||||
}
|
||||
|
||||
private function enrichConfigurationData(array $data): array
|
||||
{
|
||||
foreach ($data['SYS']['items'] as &$item) {
|
||||
if ($item['key'] === 'systemLocale') {
|
||||
$locales = Locales::getAllSystemLocales();
|
||||
if ($locales === []) {
|
||||
// Install tool operates in English context only, no xlf language label here.
|
||||
$item['description'] .= 'N/A (locale listing could not be fetched)';
|
||||
} else {
|
||||
$locales = array_map(static function ($locale) {
|
||||
return '<code>' . $locale . '</code>';
|
||||
}, $locales);
|
||||
$item['description'] .= implode(', ', $locales);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user