TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:31 +02:00
commit 3e43c11539
407 changed files with 51272 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\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'];
}
}
+46
View File
@@ -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
+58
View File
@@ -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));
}
}
+640
View File
@@ -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);
}
}
+170
View File
@@ -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,
]);
}
}
+68
View File
@@ -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,
]);
}
}
+654
View File
@@ -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