TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user