TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Sets up click-jacking prevention for HTTP requests by adding HTTP headers for the response
*
* @internal
*/
class AdditionalResponseHeaders implements MiddlewareInterface
{
/**
* Adds HTTP headers defined in $GLOBALS['TYPO3_CONF_VARS']['BE']['HTTP']['Response']['Headers']
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
// Remove HSTS header, if [BE][lockSSL] is not configured to use SSL
if ((bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] === false) {
unset($GLOBALS['TYPO3_CONF_VARS']['BE']['HTTP']['Response']['Headers']['strictTransportSecurity']);
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['BE']['HTTP']['Response']['Headers'] ?? [] as $header) {
[$headerName, $value] = explode(':', $header, 2);
$response = $response->withAddedHeader($headerName, trim($value));
}
return $response;
}
}
@@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Backend\Exception\ModuleAccessDeniedException;
use TYPO3\CMS\Backend\Exception\NoAccessibleModuleException;
use TYPO3\CMS\Backend\Module\ModuleData;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Backend\Module\ModuleProvider;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\RouteRedirect;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Validates module access and extends the PSR-7 Request with the
* resolved module object for the use in further components.
*
* @internal
*/
readonly class BackendModuleValidator implements MiddlewareInterface
{
public function __construct(
protected UriBuilder $uriBuilder,
protected ModuleProvider $moduleProvider,
protected FlashMessageService $flashMessageService,
protected TcaSchemaFactory $tcaSchemaFactory,
) {}
/**
* In case the current route targets a TYPO3 backend module and the user
* has necessary access permissions, add the module to the request.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var Route $route */
$route = $request->getAttribute('route');
$selectedSubModule = null;
$inaccessibleSubModule = null;
$ensureToPersistUserSettings = false;
$backendUser = $GLOBALS['BE_USER'] ?? null;
if (!$backendUser) {
return $handler->handle($request);
}
// Exit if access to module was denied using module access inheritance check
$inheritAccessFromModule = $route->getOption('inheritAccessFromModule');
if ($inheritAccessFromModule !== null && !$this->moduleProvider->accessGranted($inheritAccessFromModule, $backendUser)) {
return new Response(null, 403);
}
$module = $route->getOption('module');
if (!$module instanceof ModuleInterface) {
return $handler->handle($request);
}
// If on a second level module with further sub modules, jump to the third-level modules
// (either the last used or the first in the list) and store this selection for the user.
// Skip this automatic redirection if the module should show a submodule overview instead.
if ($module->getParentModule() && $module->hasSubModules() && !$module->hasSubmoduleOverview()) {
// Note: "action" is a special setting, which is evaluated here individually
$subModuleIdentifier = (string)($backendUser->getModuleData($module->getIdentifier())['action'] ?? '');
if ($module->hasSubModule($subModuleIdentifier)) {
if ($this->moduleProvider->accessGranted($subModuleIdentifier, $backendUser)) {
// Use the selected sub module if user has access to it. By checking access here,
// we prevent that the user can no longer access the parent module, since it would
// always run into the ModuleAccessDeniedException.
$selectedSubModule = $module->getSubModule($subModuleIdentifier);
} else {
// Stored sub module exists but is currently not accessible. Store the
// requested module to later inform the user about the forced redirect.
$inaccessibleSubModule = $module->getSubModule($subModuleIdentifier);
}
}
if ($selectedSubModule === null) {
// Try to fetch the first accessible sub module. We check access here to prevent
// that the user can no longer access the parent module, since it would always run
// into the ModuleAccessDeniedException.
foreach ($module->getSubModules() as $subModule) {
if ($this->moduleProvider->accessGranted($subModule->getIdentifier(), $backendUser)) {
$selectedSubModule = $subModule;
break;
}
}
}
if ($selectedSubModule !== null) {
// Overwrite the requested module and the route target if an accessible sub module has been found
$module = $selectedSubModule;
$route->setOptions(array_replace_recursive($route->getOptions(), $module->getDefaultRouteOptions()['_default']));
}
} elseif (($routeIdentifier = $route->getOption('_identifier')) !== null
&& $routeIdentifier === $module->getParentModule()?->getIdentifier()
) {
// In case the actually requested module is the parent of the actually resolved module,
// the parent module does not define a route itself and uses the current third-level module
// as fallback. Therefore, we have to check the special "action" key on the "inaccessible"
// parent module to still allow rerouting to another (last used) third-level module.
$inaccessibleParentModule = $module->getParentModule();
$subModuleIdentifier = (string)($backendUser->getModuleData($inaccessibleParentModule->getIdentifier())['action'] ?? '');
if ($inaccessibleParentModule->hasSubModule($subModuleIdentifier)) {
$module = $inaccessibleParentModule->getSubModule($subModuleIdentifier);
$route->setOptions(array_replace_recursive($route->getOptions(), $module->getDefaultRouteOptions()['_default']));
}
}
// Validate the requested module
try {
$this->validateModuleAccess($request, $module);
if ($selectedSubModule !== null && $inaccessibleSubModule !== null) {
$this->enqueueRedirectMessage($inaccessibleSubModule, $selectedSubModule);
}
} catch (ModuleAccessDeniedException $e) {
// Since the user might request a module which is just temporarily blocked, e.g. due to workspace
// restrictions, do not throw an exception but redirect to the first accessible module - if any.
if (($firstAccessibleModule = $this->moduleProvider->getFirstAccessibleModule($backendUser)) !== null) {
$this->enqueueRedirectMessage($module, $firstAccessibleModule);
return new RedirectResponse($this->uriBuilder->buildUriFromRoute($firstAccessibleModule->getIdentifier()));
}
// User does not have access to any module.. ¯\_(ツ)_/¯
throw new NoAccessibleModuleException('You don\'t have access to any module.', 1702480600);
}
// This module request (which is usually opened inside the list_frame)
// has been issued from a toplevel browser window (e.g. a link was opened in a new tab).
// Redirect to open the module as frame inside the TYPO3 backend layout.
// HEADS UP: This header will only be available in secure connections (https:// or .localhost TLD)
if ($request->getHeaderLine('Sec-Fetch-Dest') === 'document') {
return new RedirectResponse(
$this->uriBuilder->buildUriWithRedirect(
'main',
[],
RouteRedirect::createFromRoute($route, $request->getQueryParams())
)
);
}
// Third-level module, make sure to remember the previously selected module in the parent module
if ($module->getParentModule()?->getParentModule()) {
$parentModuleData = $backendUser->getModuleData($module->getParentIdentifier());
if (($parentModuleData['action'] ?? '') !== $module->getIdentifier()) {
$parentModuleData['action'] = $module->getIdentifier();
$backendUser->pushModuleData($module->getParentIdentifier(), $parentModuleData, true);
$ensureToPersistUserSettings = true;
}
}
// Check for module data, send via GET/POST parameters.
// Only consider the configured keys from the module configuration.
$requestModuleData = [];
foreach (array_keys($module->getDefaultModuleData()) as $name) {
$newValue = $request->getParsedBody()[$name] ?? $request->getQueryParams()[$name] ?? null;
if ($newValue !== null) {
$requestModuleData[$name] = $newValue;
}
}
// Get stored module data
if (!is_array(($persistedModuleData = $backendUser->getModuleData($module->getIdentifier())))) {
$persistedModuleData = [];
}
// Settings were changed from the request, so they need to get persisted
if ($requestModuleData !== []) {
$moduleData = ModuleData::createFromModule($module, array_replace_recursive($persistedModuleData, $requestModuleData));
$backendUser->pushModuleData($module->getIdentifier(), $moduleData->toArray(), true);
$ensureToPersistUserSettings = true;
} else {
$moduleData = ModuleData::createFromModule($module, $persistedModuleData);
}
// Add validated module and its data to the current request
$request = $request
->withAttribute('module', $module)
->withAttribute('moduleData', $moduleData);
$response = $handler->handle($request);
if ($ensureToPersistUserSettings) {
$backendUser->writeUC();
}
return $response;
}
/**
* Checks whether the current user is allowed to access the requested module. Does
* also evaluate page access permissions, in case an "id" is given in the request.
*
* @throws ModuleAccessDeniedException
* @throws \RuntimeException
*/
protected function validateModuleAccess(ServerRequestInterface $request, ModuleInterface $module): void
{
$backendUserAuthentication = $GLOBALS['BE_USER'];
if (!$this->moduleProvider->accessGranted($module->getIdentifier(), $backendUserAuthentication)) {
throw new ModuleAccessDeniedException('You don\'t have access to this module.', 1642450334);
}
// @todo: This misuses 'id' as a broken convention for pages-uid. The filelist module for instance
// uses 'id' as "storage-uid:path", which is only mitigated here by testing the argument
// with MU:canBeInterpretedAsInteger().
// Also see a similar misuse in extbase BackendConfigurationManager, which does this as well
// to guess a pages-uid for TypoScript retrieval.
$id = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0;
if (MathUtility::canBeInterpretedAsInteger($id) && $id > 0) {
$id = (int)$id;
$permClause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW);
// Check page access
if (!is_array(BackendUtility::readPageAccess($id, $permClause))) {
// Check if page has been deleted
if (!$this->tcaSchemaFactory->has('pages')) {
throw new \RuntimeException('You don\'t have access to this page', 1289918924);
}
$schema = $this->tcaSchemaFactory->get('pages');
if (!$schema->hasCapability(TcaSchemaCapability::SoftDelete)) {
throw new \RuntimeException('You don\'t have access to this page', 1289919924);
}
$deleteField = $schema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName();
$pageInfo = BackendUtility::getRecord('pages', $id, $deleteField, $permClause ? ' AND ' . $permClause : '', false);
if (!($pageInfo[$deleteField] ?? false)) {
throw new \RuntimeException('You don\'t have access to this page', 1289917924);
}
}
}
}
protected function enqueueRedirectMessage(ModuleInterface $requestedModule, ModuleInterface $redirectedModule): void
{
$languageService = $this->getLanguageService();
$this->flashMessageService
->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE)
->enqueue(
new FlashMessage(
sprintf(
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.message'),
$languageService->sL($redirectedModule->getTitle()),
$languageService->sL($requestedModule->getTitle())
),
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.title'),
ContextualFeedbackSeverity::INFO,
true
)
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
use TYPO3\CMS\Backend\Routing\Router;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Http\Error\MethodNotAllowedException;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Routing\RequestContextFactory;
/**
* Injects the Router and tries to match the current request with a
* configured backend route. The available backend routes were added
* in the corresponding dependency injection factories, which load
* and process the module and route configuration files
*
* - Configuration/Backend/{,Ajax}Routes.php
* - Configuration/Backend/Modules.php
*
* from each extension.
*
* After this middleware, a "Route" object is available as attribute in the
* Request object.
*
* @internal
*/
readonly class BackendRouteInitialization implements MiddlewareInterface
{
public function __construct(
protected Router $router,
protected UriBuilder $uriBuilder,
protected RequestContextFactory $requestContextFactory,
) {}
/**
* Resolve the route based on the URL path part, and also resolves a Route object
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->uriBuilder->setRequestContext($this->requestContextFactory->fromBackendRequest($request));
try {
$routeResult = $this->router->matchResult($request);
$request = $request->withAttribute('routing', $routeResult);
$request = $request->withAttribute('route', $routeResult->getRoute());
} catch (MethodNotAllowedException $e) {
return $e->createResponse();
} catch (ResourceNotFoundException $e) {
// Route not found in system
$uri = $this->uriBuilder->buildUriFromRoute('login');
return new RedirectResponse($uri);
}
return $handler->handle($request);
}
}
@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\RateLimiter\LimiterInterface;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\RouteRedirect;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\Mfa\MfaRequiredException;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Controller\ErrorPageController;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface;
use TYPO3\CMS\Core\RateLimiter\RequestRateLimitedException;
use TYPO3\CMS\Core\Session\UserSessionManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Initializes the backend user authentication object (BE_USER) and the global LANG object.
*
* @internal
*/
class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator
{
/**
* List of requests that don't need a valid BE user
*/
protected array $publicRoutes = [
'/login',
'/login/frame',
'/login/password-reset/forget',
'/login/password-reset/initiate-reset',
'/login/password-reset/validate',
'/login/password-reset/finish',
'/login/request-token',
'/install/server-response-check/host',
'/install',
'/install.php',
'/ajax/login',
'/ajax/logout',
'/ajax/login/preflight',
'/ajax/login/refresh',
'/ajax/login/timedout',
];
public function __construct(
Context $context,
private readonly LanguageServiceFactory $languageServiceFactory,
private readonly RateLimiterFactoryInterface $rateLimiterFactory,
private readonly LoggerInterface $logger,
private readonly UriBuilder $uriBuilder,
) {
parent::__construct($context);
}
/**
* Calls the bootstrap process to set up $GLOBALS['BE_USER'] AND $GLOBALS['LANG']
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var Route $route */
$route = $request->getAttribute('route');
$isAjaxCall = (bool)($route->getOption('ajax') ?? false);
// The global must be available very early, because methods below
// might trigger code which relies on it. See: #45625
$GLOBALS['BE_USER'] = GeneralUtility::makeInstance(BackendUserAuthentication::class);
// Rate Limiting
$rateLimiter = $this->ensureLoginRateLimit($GLOBALS['BE_USER'], $request);
// Whether multi-factor authentication is requested
$mfaRequested = $route->getOption('_identifier') === 'auth_mfa';
try {
$GLOBALS['BE_USER']->start($request);
} catch (MfaRequiredException $mfaRequiredException) {
// If MFA is required and we are not already on the "auth_mfa"
// route, force the user to it for further authentication.
if (!$mfaRequested && $this->isLoggedInBackendUserRequired($route)) {
if ($isAjaxCall) {
return new Response(statusCode: 401);
}
return $this->redirectToMfaEndpoint(
'auth_mfa',
$GLOBALS['BE_USER'],
$request,
['identifier' => $mfaRequiredException->getProvider()->getIdentifier()]
);
}
}
// Register the backend user as aspect and initializing workspace once for TSconfig conditions
$this->setBackendUserAspect($GLOBALS['BE_USER'], (int)($GLOBALS['BE_USER']->user['workspace_id'] ?? 0));
if ($this->isLoggedInBackendUserRequired($route)) {
if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
if ($isAjaxCall) {
return new Response(statusCode: 401);
}
$uri = $this->uriBuilder->buildUriWithRedirect(
'login',
[],
RouteRedirect::createFromRoute($route, $request->getQueryParams())
);
$response = new RedirectResponse($uri);
return $this->enrichResponseWithHeadersAndCookieInformation($request, $response, $GLOBALS['BE_USER']);
}
if (!$GLOBALS['BE_USER']->isUserAllowedToLogin()) {
$content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
'Login Error',
'TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.',
1294585860,
503
);
$response = new HtmlResponse($content, 503);
return $this->enrichResponseWithHeadersAndCookieInformation($request, $response, $GLOBALS['BE_USER']);
}
}
if ($this->context->getAspect('backend.user')->isLoggedIn()) {
$GLOBALS['BE_USER']->initializeBackendLogin($request);
// Reset the limiter after successful login
if ($rateLimiter) {
$rateLimiter->reset();
}
// In case the current request is not targeted to authenticate against MFA, the "mfa"
// key is not yet set in session (indicating that MFA has already been passed) and it's
// no "switch-user" mode, check whether the user is required to set up MFA and redirect
// to the corresponding setup endpoint if not already on it.
if (!$mfaRequested
&& !(bool)($GLOBALS['BE_USER']->getSessionData('mfa') ?? false)
&& !$GLOBALS['BE_USER']->getOriginalUserIdWhenInSwitchUserMode()
&& $GLOBALS['BE_USER']->isMfaSetupRequired()
&& $route->getOption('_identifier') !== 'setup_mfa'
) {
if ($isAjaxCall) {
return new Response(statusCode: 401);
}
return $this->redirectToMfaEndpoint('setup_mfa', $GLOBALS['BE_USER'], $request);
}
}
$GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
// Re-setting the user and take the workspace from the user object now
$this->setBackendUserAspect($GLOBALS['BE_USER']);
$response = $handler->handle($request);
$this->sessionGarbageCollection();
return $this->enrichResponseWithHeadersAndCookieInformation($request, $response, $GLOBALS['BE_USER']);
}
/**
* Backend requests should always apply Set-Cookie information and never be cacheable.
* This is also needed if there is a redirect from somewhere in the code.
*
* @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
*/
protected function enrichResponseWithHeadersAndCookieInformation(
ServerRequestInterface $request,
ResponseInterface $response,
?BackendUserAuthentication $userAuthentication
): ResponseInterface {
if ($userAuthentication) {
// If no backend user is logged-in, the cookie should be removed
if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
$userAuthentication->removeCookie();
}
// Ensure to always apply a cookie
$response = $userAuthentication->appendCookieToResponse($response, $request->getAttribute('normalizedParams'));
}
// Additional headers to never cache any PHP request should be sent at any time when
// accessing the TYPO3 Backend
$response = $this->applyHeadersToResponse($response);
return $response;
}
/**
* Garbage collection for be_sessions (with a probability)
*/
protected function sessionGarbageCollection(): void
{
UserSessionManager::create('BE')->collectGarbage();
}
/**
* Initiate a redirect to the given MFA endpoint with necessary cookies and headers appended
*/
protected function redirectToMfaEndpoint(
string $endpoint,
BackendUserAuthentication $user,
ServerRequestInterface $request,
array $parameters = []
): ResponseInterface {
$response = new RedirectResponse(
$this->uriBuilder->buildUriWithRedirect($endpoint, $parameters, RouteRedirect::createFromRequest($request))
);
// Add necessary cookies and headers to the response so
// the already passed authentication step is not lost.
$response = $user->appendCookieToResponse($response, $request->getAttribute('normalizedParams'));
$response = $this->applyHeadersToResponse($response);
return $response;
}
/**
* Check if the user is required for the request.
* If we're trying to do a login or an ajax login, don't require a user.
*
* @param Route $route the Route path to check against, something like '
* @return bool true when the Route requires an authenticated backend user
*/
protected function isLoggedInBackendUserRequired(Route $route): bool
{
return in_array($route->getPath(), $this->publicRoutes, true) === false;
}
protected function ensureLoginRateLimit(BackendUserAuthentication $user, ServerRequestInterface $request): ?LimiterInterface
{
if (!$user->isActiveLogin($request)) {
return null;
}
$loginRateLimiter = $this->rateLimiterFactory->createLoginRateLimiter($request, $user->loginType);
$limit = $loginRateLimiter->consume();
if (!$limit->isAccepted()) {
$this->logger->debug('Login request has been rate limited for IP address {ipAddress}', ['ipAddress' => $request->getAttribute('normalizedParams')->getRemoteAddress()]);
$dateformat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
$lockedUntil = $limit->getRetryAfter()->getTimestamp() > 0
? ' until ' . date($dateformat, $limit->getRetryAfter()->getTimestamp()) : '';
throw new RequestRateLimitedException(
HttpUtility::HTTP_STATUS_403,
'The login is locked' . $lockedUntil . ' due to too many failed login attempts from your IP address.',
'Login Request Rate Limited',
1616175867
);
}
return $loginRateLimiter;
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionConfiguration;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\PolicyBag;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Middleware\ResponseService;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Type\Map;
/**
* Adds Content-Security-Policy headers to response.
*
* @internal
*/
final readonly class ContentSecurityPolicyHeaders implements MiddlewareInterface
{
public function __construct(
private RequestId $requestId,
private LoggerInterface $logger,
#[Autowire(service: 'cache.assets')]
private FrontendInterface $cache,
private PolicyProvider $policyProvider,
private ResponseService $responseService,
private DirectiveHashCollection $directiveHashCollection,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$scope = Scope::backend();
$nonce = $this->requestId->nonce;
$request = $request->withAttribute('nonce', $nonce);
$response = $handler->handle($request);
$disposition = Disposition::enforce;
$dispositionMap = new Map();
$dispositionMap[$disposition] = new DispositionConfiguration(
true,
true,
$GLOBALS['TYPO3_CONF_VARS'][$scope->type->abbreviate()]['contentSecurityPolicyReportingUrl'],
);
$policyBag = new PolicyBag($scope, $dispositionMap, new Behavior(), $nonce, $this->directiveHashCollection);
if ($response->hasHeader('Content-Security-Policy') || $response->hasHeader('Content-Security-Policy-Report-Only')) {
$this->logger->info('Content-Security-Policy not enforced due to existence of custom header', [
'scope' => (string)$scope,
'uri' => (string)$request->getUri(),
]);
return $response;
}
$this->policyProvider->prepare($policyBag, $request, $response);
$policy = $policyBag->getPolicy($disposition);
if ($policy->isEmpty()) {
return $response;
}
if ($policyBag->behavior->useNonce === false) {
$response = $this->responseService->dropNonceFromHtmlResponse($response, $nonce);
}
return $response->withHeader(
$disposition->getHttpHeaderName(),
$policy->compile($policyBag, $this->cache)
);
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Middleware\AbstractContentSecurityPolicyReporter;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
/**
* @internal
*/
class ContentSecurityPolicyReporter extends AbstractContentSecurityPolicyReporter
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$scope = Scope::backend();
if ($this->targetsCspReportUri($scope, $request)) {
if (!$this->isCspReport($scope, $request)) {
return new HtmlResponse('Submission to CSP reporting endpoint denied', 403);
}
// @todo check/store headers `origin` + `referer`
// @todo create report, then call persist, then dispatch new event
$this->persistCspReport($scope, $request);
return (new Response())->withStatus(201);
}
return $handler->handle($request);
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\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\RedirectResponse;
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
/**
* Check lockSSL configuration variable and redirect
* to https version of the backend if needed
*
* Depends on the NormalizedParams middleware to identify the
* Site URL and if the page is not running via HTTPS yet.
*
* @internal
*/
readonly class ForcedHttpsBackendRedirector implements MiddlewareInterface
{
public function __construct(
protected BackendEntryPointResolver $backendEntryPointResolver
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ((bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] && !$request->getAttribute('normalizedParams')->isHttps()) {
if ((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort']) {
$sslPortSuffix = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort'];
} else {
$sslPortSuffix = null;
}
$backendUrl = $this->backendEntryPointResolver->getUriFromRequest($request)
->withScheme('https')
->withPort($sslPortSuffix);
return new RedirectResponse($backendUrl);
}
return $handler->handle($request);
}
}
@@ -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\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Page\Event\ResolveVirtualJavaScriptImportEvent;
/**
* @internal
*/
final readonly class JavaScriptLabelImportMapEntryResolver implements MiddlewareInterface
{
public function __construct(
private ListenerProvider $listenerProvider,
private UriBuilder $uriBuilder,
#[Autowire(expression: 'service("package-dependent-cache-identifier").withAdditionalHashedIdentifier("JavaScriptLanguageDomain").toString()')]
private string $javaScriptLanguageDomainCacheIdentifier,
) {}
/**
* Adds HTTP headers defined in $GLOBALS['TYPO3_CONF_VARS']['BE']['HTTP']['Response']['Headers']
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->listenerProvider->addListener(
ResolveVirtualJavaScriptImportEvent::class,
self::class,
'resolveVirtualLabelImport'
);
return $handler->handle($request);
}
public function resolveVirtualLabelImport(ResolveVirtualJavaScriptImportEvent $event): void
{
if ($event->resolution === null && $event->virtualName === 'labels/') {
$path = (string)$this->uriBuilder->buildUriFromRoute('language_domain', [
'locale' => $GLOBALS['LANG']->getLocale()?->getName() ?? 'en',
'cacheBustInfix' => $this->javaScriptLanguageDomainCacheIdentifier,
'domain' => '__DOMAIN__',
]);
// domain identifier will be aded via JavaScript importmap prefix handling, strip it to generate
// a base identifier
$path = str_replace('__DOMAIN__', '', $path);
$event->resolution = $path;
}
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Backend\Authentication\BackendLocker;
use TYPO3\CMS\Backend\Exception\BackendAccessDeniedException;
use TYPO3\CMS\Backend\Exception\BackendLockedException;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Checks various security options for accessing the TYPO3 backend before proceeding
*
* Depends on the NormalizedParams middleware to identify the
* Site URL and if the page is not running via HTTPS yet.
*
* @internal
*/
readonly class LockedBackendGuard implements MiddlewareInterface
{
public function __construct(
protected BackendLocker $lockService
) {}
/**
* Checks the client's IP address and the availability of LOCK_BACKEND file,
* location may vary, @see BackendLocker->isLocked().
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$redirectToUri = $this->checkLockedBackend();
if (!empty($redirectToUri)) {
return new RedirectResponse($redirectToUri, 302);
}
} catch (BackendLockedException $e) {
// Looks like an AJAX request that can handle JSON, (usually from the timeout functionality)
// So, let's form a request that fits
if (str_contains($request->getHeaderLine('Accept'), 'application/json')) {
$session = [
'timed_out' => false,
'will_time_out' => false,
'locked' => true,
'message' => $e->getMessage(),
];
return new JsonResponse(['login' => $session]);
}
throw $e;
}
$this->validateVisitorsIpAgainstIpMaskList(
$request->getAttribute('normalizedParams')->getRemoteAddress(),
trim((string)$GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList'])
);
return $handler->handle($request);
}
/**
* Check adminOnly configuration variable and redirects to an URL in file
* LOCK_BACKEND. Location may vary, @see BackendLocker->isLocked().
*
* @throws BackendLockedException
*/
protected function checkLockedBackend(): ?string
{
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'] < 0) {
throw new BackendLockedException(
HttpUtility::HTTP_STATUS_403,
'Backend is locked for maintenance. [BE][adminOnly] is set to "' . (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'] . '".',
'TYPO3 Backend locked',
1517949794
);
}
if ($this->lockService->isLocked()) {
$redirectUri = $this->lockService->getRedirectUriFromLockContents();
if ($redirectUri) {
return $redirectUri;
}
throw new BackendLockedException(
HttpUtility::HTTP_STATUS_403,
'Backend access by browser is locked for maintenance. Remove lock by removing the file "LOCK_BACKEND" as configured in TYPO3_CONF_VARS[BE][lockBackendFile]. Or (better) use CLI-script "bin/typo3 backend:unlock".',
'TYPO3 Backend locked',
1517949793
);
}
return null;
}
/**
* Compare client IP with IPmaskList and throw an exception
*/
protected function validateVisitorsIpAgainstIpMaskList(string $ipAddress, string $ipMaskList = ''): void
{
if ($ipMaskList !== '' && !GeneralUtility::cmpIP($ipAddress, $ipMaskList)) {
throw new BackendAccessDeniedException(
HttpUtility::HTTP_STATUS_403,
'The IP address of your client does not match the list of allowed IP addresses.',
'TYPO3 Backend access denied',
1517949792
);
}
}
}
@@ -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\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Context\PageContextFactory;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Initializes PageContext for backend modules that work with pages.
*
* Creates a PageContext with resolved language information and stores it
* in the request attribute 'pageContext' for use by controllers.
*
* This middleware only runs for modules that use the page tree navigation
* component ('@typo3/backend/tree/page-tree-element'). This includes modules
* like web_layout, records, and others under the 'content' parent, as well
* as standalone modules that explicitly set the navigation component.
*
* The middleware:
* - Checks if the module or any of its parents use the page tree component
* - Extracts the page ID from request (query or body parameter 'id')
* - Defaults to page ID 0 (NullSite/root level) if not found
* - Creates PageContext with language resolution
*
* If PageContext creation fails (e.g. no site found, no page access),
* the middleware logs a warning but continues without breaking the request,
* allowing controllers to handle the missing context or create it manually.
*
* @internal
*/
readonly class PageContextInitialization implements MiddlewareInterface
{
public function __construct(
protected PageContextFactory $pageContextFactory,
protected LoggerInterface $logger,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$this->requiresPageContext($request)) {
return $handler->handle($request);
}
$backendUser = $request->getAttribute('backend.user', $GLOBALS['BE_USER']);
// Only process if user is authenticated in the backend
if (!($backendUser instanceof BackendUserAuthentication) || !$backendUser->user) {
return $handler->handle($request);
}
$pageId = $this->determinePageId($request);
try {
$request = $request->withAttribute(
'pageContext',
$this->pageContextFactory->createFromRequest($request, $pageId, $backendUser)
);
} catch (\Exception $e) {
// If PageContext creation fails, log the error and continue without it.
// Controllers can fall back to manual creation if needed.
$this->logger->warning(
'Failed to create PageContext in middleware for page {page}: {error}',
[
'page' => $pageId,
'error' => $e->getMessage(),
'exception' => $e,
]
);
}
return $handler->handle($request);
}
private function determinePageId(ServerRequestInterface $request): int
{
$id = $request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? null;
if ($id !== null) {
return (int)$id;
}
$editStatement = $request->getParsedBody()['edit'] ?? $request->getQueryParams()['edit'] ?? null;
if (is_array($editStatement)) {
$table = key($editStatement);
$uidAndAction = current($editStatement);
$uid = (int)key($uidAndAction);
$action = current($uidAndAction);
if ($action === 'edit') {
return $this->getPageIdByRecord($table, $uid);
}
if ($action === 'new') {
return $this->getPageIdByRecord($table, $uid, true);
}
}
$commandStatement = $request->getParsedBody()['cmd'] ?? $request->getQueryParams()['cmd'] ?? null;
if (is_array($commandStatement)) {
$table = key($commandStatement);
$uidActionAndTarget = current($commandStatement);
$uid = (int)key($uidActionAndTarget);
$actionAndTarget = current($uidActionAndTarget);
$action = key($actionAndTarget);
$target = current($actionAndTarget);
if ($action === 'delete') {
return $this->getPageIdByRecord($table, $uid);
}
if ($action === 'copy' || $action === 'move') {
return $this->getPageIdByRecord($table, (int)($target['target'] ?? $target), true);
}
}
return 0;
}
private function getPageIdByRecord(string $table, int $id, bool $ignoreTable = false): int
{
$pageId = 0;
if ($table && $id) {
if (($ignoreTable || $table === 'pages') && $id >= 0) {
$pageId = $id;
} else {
$record = BackendUtility::getRecordWSOL($table, abs($id), '*', '', false);
$pageId = (int)($record['pid'] ?? 0);
}
}
return $pageId;
}
/**
* Check if the current request requires PageContext initialization.
*
* PageContext is required when:
* 1. Module uses the page tree navigation component
* 2. Route explicitly has 'requestPageContext' option set to true
*/
private function requiresPageContext(ServerRequestInterface $request): bool
{
return ((($module = $request->getAttribute('module')) instanceof ModuleInterface) && $module->getNavigationComponent() === '@typo3/backend/tree/page-tree-element')
|| ((($route = $request->getAttribute('route')) instanceof Route) && $route->getOption('requestPageContext'));
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Routing\SiteMatcher;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Usually called after the route object is resolved, however, this is not possible yet as this happens
* within the RequestHandler/RouteDispatcher right now and should go away.
*
* This middleware checks for a "id" parameter. If present, it adds a site information to this page ID.
*
* Very useful for all "Web" related modules to resolve all available languages for a site.
*/
readonly class SiteResolver implements MiddlewareInterface
{
public function __construct(
private SiteMatcher $siteMatcher
) {}
/**
* Resolve the site information by checking the page ID ("id" parameter) which is typically
* used in BE modules of type "web".
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$pageId = ($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0);
if (!MathUtility::canBeInterpretedAsInteger($pageId)) {
// @todo: The "filelist" module abuses "id" to carry a storage like "1:/" around. This
// should be changed. To *always* have a site attribute attached to the request,
// we for now resolve to zero here, leading to NullSite object.
// Change "filelist" module to no longer abuse "id" GET argument and throw an
// exception here if $pageUid can not be resolved to an int.
$pageId = 0;
}
$pageId = (int)$pageId;
$rootLine = null;
if ($pageId > 0) {
$rootLine = BackendUtility::BEgetRootLine($pageId);
}
$site = $this->siteMatcher->matchByPageId($pageId, $rootLine);
$request = $request->withAttribute('site', $site);
return $handler->handle($request);
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Controller\Security\SudoModeController;
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessStorage;
use TYPO3\CMS\Backend\Security\SudoMode\Exception\RequestGrantedException;
use TYPO3\CMS\Backend\Security\SudoMode\Exception\VerificationRequiredException;
use TYPO3\CMS\Core\Http\Application;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
/**
* Middleware that catches any `VerificationRequiredException` (= the current
* user must verify the access for a particular resource, route, module) by
* entering their password again; and any `RequestGrantedException` (= the
* verification process was successful & the user shall be redirected to
* the URI, that has been requested originally).
*/
final class SudoModeInterceptor implements MiddlewareInterface
{
/**
* @internal
*/
public ?ServerRequestInterface $currentRequest = null;
public function __construct(
private readonly AccessStorage $storage,
private readonly SudoModeController $controller,
private readonly ServerRequestFactoryInterface $serverRequestFactory,
private readonly Application $application,
private readonly LoggerInterface $logger,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->currentRequest = $request;
try {
$response = $handler->handle($request);
} catch (VerificationRequiredException $exception) {
$response = $this->handleVerificationRequired($exception, $request);
} catch (RequestGrantedException $exception) {
$response = $this->handleRequestGrantedException($exception, $request);
}
$this->currentRequest = null;
return $response;
}
/**
* Redirects to the sudo mode controller, and renders the password verification dialog.
*/
private function handleVerificationRequired(
VerificationRequiredException $exception,
ServerRequestInterface $request,
): ResponseInterface {
$claim = $exception->getClaim();
$this->logger->info('Confirmation required', ['claim' => $claim->id]);
$this->storage->addClaim($claim);
$isAjaxCall = (bool)($request->getAttribute('route')?->getOption('ajax') ?? false);
if ($isAjaxCall) {
return (new JsonResponse([
'sudoModeInitialization' => [
'verifyActionUri' => (string)$this->controller->buildVerifyActionUriForClaim($claim),
'allowInstallToolPassword' => $GLOBALS['BE_USER']->isSystemMaintainer(),
'isAjax' => true,
'labels' => $GLOBALS['LANG']->getLabelsFromResource('EXT:backend/Resources/Private/Language/SudoMode.xlf'),
],
]))->withStatus(422, 'Step-Up required: A different authentication level is required');
}
$uri = $this->controller->buildModuleActionUriForClaim($claim);
return new RedirectResponse($uri, 401);
}
/**
* Redirects (GET) or Subrequests (non GET HTTP methods) to the URI that
* was originally requested (prior to this sudo mode interception).
*/
private function handleRequestGrantedException(
RequestGrantedException $exception,
ServerRequestInterface $request,
): ResponseInterface {
$instruction = $exception->getInstruction();
if ($instruction->getMethod() === 'GET') {
return new RedirectResponse($instruction->getUri(), 303);
}
$request = $this->serverRequestFactory
->createServerRequest(
$instruction->getMethod(),
$instruction->getUri(),
$instruction->getServerParams()
)
->withBody($instruction->getBody())
->withParsedBody($instruction->getParsedBody())
->withQueryParams($instruction->getQueryParams())
->withRequestTarget($instruction->getRequestTarget())
// Use cookie params from current request, as cookies might have been updated in the meantime
->withCookieParams($request->getCookieParams());
foreach ($instruction->getHeaders() as $name => $values) {
foreach ($values as $value) {
$request = $request->withAddedHeader($name, $value);
}
}
return $this->application->handle($request);
}
}