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
+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);
}
}