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
+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\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
/**
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
final readonly class AssetPublishing implements MiddlewareInterface
{
public function __construct(
private PackageManager $packageManager,
private SystemResourcePublisherInterface $resourcePublisher,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
foreach ($this->packageManager->getActivePackages() as $package) {
if ($package->isPartOfMinimalUsableSystem()) {
$this->resourcePublisher->publishResources($package);
}
}
return $handler->handle($request);
}
}
+162
View File
@@ -0,0 +1,162 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Install\Controller\InstallerController;
use TYPO3\CMS\Install\Service\EnableFileService;
use TYPO3\CMS\Install\Service\LateBootService;
use TYPO3\CMS\Install\Service\SessionService;
/**
* Middleware to walk through the web installation process of TYPO3
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
readonly class Installer implements MiddlewareInterface
{
public function __construct(
private LateBootService $lateBootService,
private FormProtectionFactory $formProtectionFactory,
private SessionService $sessionService
) {}
/**
* Handles an Install Tool request when nothing is there
*
* @throws \RuntimeException
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$this->canHandleRequest()) {
return $handler->handle($request);
}
// This is required for icon API, that still has no way to pass
// a request/ normalizedParams to the icon URL generation
$GLOBALS['TYPO3_REQUEST'] = $request;
$container = $this->lateBootService->getContainer();
$backup = $this->lateBootService->makeCurrent($container);
// Lazy load InstallerController, to instantiate the class and the dependencies only if we handle an install request.
$controller = $container->get(InstallerController::class);
$actionName = $request->getParsedBody()['install']['action'] ?? $request->getQueryParams()['install']['action'] ?? 'init';
$action = $actionName . 'Action';
if ($actionName === 'init' || $actionName === 'mainLayout') {
$response = $controller->$action($request);
} elseif ($actionName === 'checkInstallerAvailable') {
$response = new JsonResponse([
'success' => $this->isInstallerAvailable(),
]);
} elseif ($actionName === 'showInstallerNotAvailable') {
$response = $controller->showInstallerNotAvailableAction($request);
} elseif ($actionName === 'checkEnvironmentAndFolders'
|| $actionName === 'showEnvironmentAndFolders'
|| $actionName === 'executeEnvironmentAndFolders'
) {
$this->throwIfInstallerIsNotAvailable();
$response = $controller->$action($request);
} else {
$this->throwIfInstallerIsNotAvailable();
// With main folder layout available, sessions can be handled
$this->sessionService->installSessionHandler($request);
$this->sessionService->startSession();
if ($this->sessionService->isExpired($request)) {
$this->sessionService->refreshSession();
}
$postValues = $request->getParsedBody()['install'] ?? [];
$sessionTokenOk = false;
if (empty($postValues)) {
// No post data is there, no token check necessary
$sessionTokenOk = true;
}
if (isset($postValues['token'])) {
// A token must be given as soon as there is POST data
$formProtection = $this->formProtectionFactory->createFromRequest($request);
if ($actionName === '') {
throw new \RuntimeException('No POST action given for token check', 1505647681);
}
$sessionTokenOk = $formProtection->validateToken($postValues['token'], 'installTool', $actionName);
}
if (!$sessionTokenOk) {
$this->sessionService->resetSession();
$this->sessionService->startSession();
throw new \RuntimeException('Invalid session token', 1505647737);
}
if (!method_exists($controller, $action)) {
// Sanitize action method, preventing injecting whatever method name
throw new \RuntimeException(
'Unknown action method ' . $action . ' in controller InstallerController',
1505687700
);
}
$response = $controller->$action($request);
if ($actionName === 'executeDefaultConfiguration') {
// Executing last step cleans session
$this->sessionService->destroySession($request);
}
}
$this->lateBootService->makeCurrent(null, $backup);
return $response;
}
/**
* First installation is in progress, if system/settings.php does not exist,
* or if FIRST_INSTALL file exists.
*/
protected function canHandleRequest(): bool
{
$localConfigurationFileLocation = (new ConfigurationManager())->getSystemConfigurationFileLocation();
return !@is_file($localConfigurationFileLocation) || EnableFileService::isFirstInstallAllowed();
}
/**
* @throws \RuntimeException If installer is not available due to missing FIRST_INSTALL
*/
protected function throwIfInstallerIsNotAvailable()
{
if (!$this->isInstallerAvailable()) {
throw new \RuntimeException(
'Installer not available',
1505637427
);
}
}
/**
* @return bool TRUE if FIRST_INSTALL file exists
*/
protected function isInstallerAvailable(): bool
{
if (EnableFileService::isFirstInstallAllowed()) {
return true;
}
return false;
}
}
@@ -0,0 +1,72 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Http\ResponseFactory;
use TYPO3\CMS\Core\Http\StreamFactory;
use TYPO3\CMS\Core\Localization\JavaScriptLanguageDomainProvider as CoreJavaScriptLanguageDomainProvider;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Localization\TranslationDomainResolver;
/**
* @internal
*/
final readonly class JavaScriptLanguageDomainProvider implements MiddlewareInterface
{
public function __construct(
private LanguageServiceFactory $languageServiceFactory,
private TranslationDomainResolver $translationDomainResolver,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$data = $request->getQueryParams()['install'] ?? [];
if (!is_array($data)
|| ($data['action'] ?? null) !== 'labels'
|| !is_string(($data['domain'] ?? null))
) {
return $handler->handle($request);
}
$domain = $data['domain'];
$locale = 'en';
// Only labels from core/backend/install are allowed to
// disallow enumeration of available extensions
$isValidDomain = (
str_starts_with($domain, 'core.')
|| str_starts_with($domain, 'backend.')
|| str_starts_with($domain, 'install.')
);
if (!$isValidDomain) {
return (new ResponseFactory())->createResponse(403);
}
$provider = new CoreJavaScriptLanguageDomainProvider(
$this->languageServiceFactory,
$this->translationDomainResolver,
new ResponseFactory(),
new StreamFactory(),
);
return $provider->createLanguageDomainResponse($domain, $locale);
}
}
+333
View File
@@ -0,0 +1,333 @@
<?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\Middleware;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\Security\ReferrerEnforcer;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Package\FailsafePackageManager;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Install\Authentication\AuthenticationService;
use TYPO3\CMS\Install\Controller\AbstractController;
use TYPO3\CMS\Install\Controller\EnvironmentController;
use TYPO3\CMS\Install\Controller\IconController;
use TYPO3\CMS\Install\Controller\LayoutController;
use TYPO3\CMS\Install\Controller\LoginController;
use TYPO3\CMS\Install\Controller\MaintenanceController;
use TYPO3\CMS\Install\Controller\SettingsController;
use TYPO3\CMS\Install\Controller\UpgradeController;
use TYPO3\CMS\Install\Service\EnableFileService;
use TYPO3\CMS\Install\Service\SessionService;
/**
* Default middleware for all requests inside the TYPO3 Install Tool, which does a simple hardcoded
* dispatching to a controller based on the get/post variable.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class Maintenance implements MiddlewareInterface
{
/**
* @var array List of valid controllers
*/
protected array $controllers = [
'icon' => IconController::class,
'layout' => LayoutController::class,
'login' => LoginController::class,
'maintenance' => MaintenanceController::class,
'settings' => SettingsController::class,
'upgrade' => UpgradeController::class,
'environment' => EnvironmentController::class,
];
public function __construct(
protected readonly FailsafePackageManager $packageManager,
protected readonly ConfigurationManager $configurationManager,
protected readonly PasswordHashFactory $passwordHashFactory,
protected readonly ContainerInterface $container,
protected readonly FormProtectionFactory $formProtectionFactory,
protected readonly SessionService $sessionService
) {}
/**
* Handles an Install Tool request for normal operations
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$this->canHandleRequest()) {
return $handler->handle($request);
}
if (($GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'] ?? '') === '') {
return new HtmlResponse('$GLOBALS[\'TYPO3_CONF_VARS\'][\'BE\'][\'installToolPassword\'] must not be empty.', 500);
}
// This is required for icon API, that still has no way to pass
// a request/ normalizedParams to the icon URL generation
$GLOBALS['TYPO3_REQUEST'] = $request;
$controllerName = $request->getQueryParams()['install']['controller'] ?? 'layout';
$actionName = $request->getParsedBody()['install']['action'] ?? $request->getQueryParams()['install']['action'] ?? 'init';
if ($actionName === 'showEnableInstallToolFile' && EnableFileService::isInstallToolEnableFilePermanent()) {
$actionName = 'showLogin';
}
$action = $actionName . 'Action';
// not session related actions
if ($actionName === 'init') {
$controller = $this->container->get(LayoutController::class);
return $controller->initAction($request);
}
if ($actionName === 'checkEnableInstallToolFile') {
return new JsonResponse([
'success' => $this->checkEnableInstallToolFile(),
]);
}
if ($actionName === 'showEnableInstallToolFile') {
$controller = $this->container->get(LoginController::class);
return $controller->showEnableInstallToolFileAction($request);
}
if ($actionName === 'showLogin') {
if (!$this->checkEnableInstallToolFile()) {
throw new \RuntimeException('Not authorized', 1505564888);
}
$controller = $this->container->get(LoginController::class);
return $controller->showLoginAction($request);
}
$this->sessionService->installSessionHandler($request);
// the backend user has an active session but the admin / maintainer
// rights have been revoked or the user was disabled or deleted in the meantime
if ($this->sessionService->isAuthorizedBackendUserSession($request) && !$this->sessionService->hasActiveBackendUserRoleAndSession()) {
// log out the user and destroy the session
$this->sessionService->resetSession();
$this->sessionService->destroySession($request);
$formProtection = $this->formProtectionFactory->createFromRequest($request);
$formProtection->clean();
return new HtmlResponse('', 403);
}
if ($actionName === 'preAccessCheck') {
$response = new JsonResponse([
'installToolLocked' => !$this->checkEnableInstallToolFile(),
'isAuthorized' => $this->sessionService->isAuthorized($request),
]);
} elseif ($actionName === 'checkLogin') {
if (!$this->checkEnableInstallToolFile() && !$this->sessionService->isAuthorizedBackendUserSession($request)) {
throw new \RuntimeException('Not authorized', 1505563556);
}
if ($this->sessionService->isAuthorized($request)) {
$this->sessionService->refreshSession();
$response = new JsonResponse([
'success' => true,
]);
} else {
// Session expired, log out user, start new session
$this->sessionService->resetSession();
$this->sessionService->startSession();
$response = new JsonResponse([
'success' => false,
]);
}
} elseif ($actionName === 'login') {
$this->sessionService->initializeSession();
if (!$this->checkEnableInstallToolFile()) {
throw new \RuntimeException('Not authorized', 1505567462);
}
$this->checkSessionToken($request);
$this->checkSessionLifetime($request);
$password = $request->getParsedBody()['install']['password'] ?? null;
$authService = $this->container->get(AuthenticationService::class);
if ($authService->loginWithPassword($password, $request, $this->sessionService)) {
$response = new JsonResponse([
'success' => true,
]);
} else {
if ($password === null || empty($password)) {
$messageQueue = new FlashMessageQueue('install');
$messageQueue->enqueue(
new FlashMessage('Please enter the install tool password', '', ContextualFeedbackSeverity::ERROR)
);
} else {
$hashInstance = $this->passwordHashFactory->getDefaultHashInstance('BE');
$hashedPassword = $hashInstance->getHashedPassword($password);
$messageQueue = new FlashMessageQueue('install');
$messageQueue->enqueue(
new FlashMessage(
'Given password does not match the install tool login password. Calculated hash: ' . $hashedPassword,
'',
ContextualFeedbackSeverity::ERROR
)
);
}
$response = new JsonResponse([
'success' => false,
'status' => $messageQueue,
]);
}
} elseif ($actionName === 'logout') {
if (EnableFileService::installToolEnableFileExists() && !EnableFileService::isInstallToolEnableFilePermanent()) {
EnableFileService::removeInstallToolEnableFile();
}
$formProtection = $this->formProtectionFactory->createFromRequest($request);
$formProtection->clean();
$this->sessionService->destroySession($request);
$response = new JsonResponse([
'success' => true,
]);
} else {
$enforceReferrerResponse = $this->enforceReferrer($request);
if ($enforceReferrerResponse !== null) {
return $enforceReferrerResponse;
}
$this->sessionService->initializeSession();
if (
!$this->checkSessionToken($request)
|| !$this->checkSessionLifetime($request)
|| !$this->sessionService->isAuthorized($request)
) {
return new HtmlResponse('', 403);
}
$this->sessionService->refreshSession();
if (!array_key_exists($controllerName, $this->controllers)) {
throw new \RuntimeException(
'Unknown controller ' . $controllerName,
1505215756
);
}
$this->packageManager->recreatePackageStatesFileIfMissing();
$className = $this->controllers[$controllerName];
/** @var AbstractController $controller */
$controller = $this->container->get($className);
if (!method_exists($controller, $action)) {
throw new \RuntimeException(
'Unknown action method ' . $action . ' in controller ' . $controllerName,
1505216027
);
}
$response = $controller->$action($request);
}
return $response;
}
/**
* This request handler is only accessible when basic system integrity constraints are fulfilled.
*/
protected function canHandleRequest(): bool
{
$basicIntegrity = $this->checkIfEssentialConfigurationExists() && !EnableFileService::isFirstInstallAllowed();
if (!$basicIntegrity) {
return false;
}
return true;
}
/**
* Checks if ENABLE_INSTALL_TOOL exists.
*/
protected function checkEnableInstallToolFile(): bool
{
return EnableFileService::checkInstallToolEnableFile();
}
/**
* Use form protection API to find out if protected POST forms are ok.
*/
protected function checkSessionToken(ServerRequestInterface $request): bool
{
$postValues = $request->getParsedBody()['install'] ?? null;
// no post data is there, so no token check necessary
if (empty($postValues)) {
return true;
}
$tokenOk = false;
// A token must be given as soon as there is POST data
if (isset($postValues['token'])) {
$formProtection = $this->formProtectionFactory->createFromRequest($request);
$action = (string)$postValues['action'];
if ($action === '') {
throw new \RuntimeException(
'No POST action given for token check',
1369326593
);
}
$tokenOk = $formProtection->validateToken($postValues['token'], 'installTool', $action);
}
if (!$tokenOk) {
$this->sessionService->resetSession();
$this->sessionService->startSession();
}
return $tokenOk;
}
/**
* Check if session expired.
* If the session has expired, the login form is displayed.
*
* @return bool True if session lifetime is OK
*/
protected function checkSessionLifetime(ServerRequestInterface $request): bool
{
$isExpired = $this->sessionService->isExpired($request);
if ($isExpired) {
// Session expired, log out user, start new session
$this->sessionService->resetSession();
$this->sessionService->startSession();
}
return !$isExpired;
}
/**
* Check if system/settings.php exists (PackageStates is optional)
*
* @return bool TRUE when the essential configuration is available, otherwise FALSE
*/
protected function checkIfEssentialConfigurationExists(): bool
{
return file_exists($this->configurationManager->getSystemConfigurationFileLocation());
}
/**
* Evaluates HTTP `Referer` header (which is denied by client to be a custom
* value) - attempts to ensure the value is given using a HTML client refresh.
* see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
*/
protected function enforceReferrer(ServerRequestInterface $request): ?ResponseInterface
{
if (!(new Features())->isFeatureEnabled('security.backend.enforceReferrer')) {
return null;
}
return (new ReferrerEnforcer())->handle($request, [
'flags' => ['refresh-always'],
'subject' => 'Install Tool',
]);
}
}