TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||
use TYPO3\CMS\Core\Context\VisibilityAspect;
|
||||
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFactory;
|
||||
use TYPO3\CMS\Core\Http\AbstractApplication;
|
||||
use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
|
||||
|
||||
/**
|
||||
* Entry point for the TYPO3 Backend (HTTP requests)
|
||||
*/
|
||||
class Application extends AbstractApplication
|
||||
{
|
||||
public function __construct(
|
||||
#[AutowireInline(
|
||||
class: MiddlewareDispatcher::class,
|
||||
arguments: [
|
||||
'$kernel' => '@' . RequestHandler::class,
|
||||
'$middlewares' => '@backend.middlewares',
|
||||
],
|
||||
)]
|
||||
RequestHandlerInterface $requestHandler,
|
||||
protected readonly Context $context,
|
||||
) {
|
||||
$this->requestHandler = $requestHandler;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
|
||||
// Set up the initial context
|
||||
$this->initializeContext();
|
||||
return parent::handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Context used for accessing data and finding out the current state of the application
|
||||
*/
|
||||
protected function initializeContext(): void
|
||||
{
|
||||
$this->context->setAspect('date', new DateTimeAspect(DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME'])));
|
||||
$this->context->setAspect('visibility', new VisibilityAspect(true, true, false, true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Backend\Resource\PublicUrlPrefixer;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\InvalidRequestTokenException;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\MissingRequestTokenException;
|
||||
use TYPO3\CMS\Backend\Routing\Route;
|
||||
use TYPO3\CMS\Backend\Routing\RouteRedirect;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
use TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent;
|
||||
|
||||
/**
|
||||
* General RequestHandler for the TYPO3 Backend. This is used for all Backend requests, including AJAX routes.
|
||||
*
|
||||
* If a get/post parameter "route" is set, the Backend Routing is called and searches for a
|
||||
* matching route inside the Router. The corresponding controller / action is called then which returns the response.
|
||||
*
|
||||
* The following get/post parameters are evaluated here:
|
||||
* - route
|
||||
* - token
|
||||
*/
|
||||
class RequestHandler implements RequestHandlerInterface
|
||||
{
|
||||
protected RouteDispatcher $dispatcher;
|
||||
|
||||
protected UriBuilder $uriBuilder;
|
||||
|
||||
protected ListenerProvider $listenerProvider;
|
||||
|
||||
public function __construct(
|
||||
RouteDispatcher $dispatcher,
|
||||
UriBuilder $uriBuilder,
|
||||
ListenerProvider $listenerProvider
|
||||
) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->uriBuilder = $uriBuilder;
|
||||
$this->listenerProvider = $listenerProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a backend request, after finishing running middlewares
|
||||
* Dispatch the request to the appropriate controller through the
|
||||
* Backend Dispatcher which resolves the routing
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Make sure all FAL resources have absolute URL paths
|
||||
$this->listenerProvider->addListener(
|
||||
GeneratePublicUrlForResourceEvent::class,
|
||||
PublicUrlPrefixer::class,
|
||||
'prefixWithSitePath'
|
||||
);
|
||||
|
||||
/** @var Route $route */
|
||||
$route = $request->getAttribute('route');
|
||||
$isAjaxCall = (bool)($route->getOption('ajax') ?? false);
|
||||
|
||||
// b/w compat
|
||||
$GLOBALS['TYPO3_REQUEST'] = $request;
|
||||
|
||||
try {
|
||||
// Check if the router has the available route and dispatch.
|
||||
return $this->dispatcher->dispatch($request);
|
||||
} catch (MissingRequestTokenException $e) {
|
||||
if ($isAjaxCall) {
|
||||
return new Response(statusCode: 401);
|
||||
}
|
||||
// When token was missing, then redirect to login, but keep the current route as redirect after login
|
||||
$loginUrl = $this->uriBuilder->buildUriWithRedirect(
|
||||
'login',
|
||||
[],
|
||||
RouteRedirect::createFromRoute($request->getAttribute('route'), $request->getQueryParams())
|
||||
);
|
||||
return new RedirectResponse($loginUrl);
|
||||
} catch (InvalidRequestTokenException $e) {
|
||||
if ($isAjaxCall) {
|
||||
return new Response(statusCode: 401);
|
||||
}
|
||||
// When token was invalid, then redirect to login
|
||||
$loginForm = $this->uriBuilder->buildUriFromRoute('login');
|
||||
return new RedirectResponse($loginForm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\InvalidRequestTokenException;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\MissingRequestTokenException;
|
||||
use TYPO3\CMS\Backend\Routing\Route;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessFactory;
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessStorage;
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Event\SudoModeRequiredEvent;
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Exception\VerificationRequiredException;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\Dispatcher;
|
||||
use TYPO3\CMS\Core\Http\Error\MethodNotAllowedException;
|
||||
use TYPO3\CMS\Core\Http\Security\ReferrerEnforcer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Dispatcher which resolves a route to call a controller and method (but also a callable)
|
||||
*/
|
||||
class RouteDispatcher extends Dispatcher
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly FormProtectionFactory $formProtectionFactory,
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly AccessFactory $factory,
|
||||
protected readonly AccessStorage $storage,
|
||||
protected readonly Features $features,
|
||||
protected readonly ReferrerEnforcer $referrerEnforcer,
|
||||
ContainerInterface $container,
|
||||
) {
|
||||
parent::__construct($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method checks the target of the route, and tries to call it.
|
||||
*
|
||||
* @param ServerRequestInterface $request the current server request
|
||||
* @return ResponseInterface the filled response by the callable / controller/action
|
||||
* @throws InvalidRequestTokenException if the route requested a token, but this token did not match
|
||||
* @throws MissingRequestTokenException if the route requested a token, but there was none
|
||||
* @throws \InvalidArgumentException if the defined target for the route is invalid
|
||||
*/
|
||||
public function dispatch(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $request->getAttribute('route');
|
||||
|
||||
$enforceReferrerResponse = $this->enforceReferrer($request, $route);
|
||||
if ($enforceReferrerResponse !== null) {
|
||||
return $enforceReferrerResponse;
|
||||
}
|
||||
// Ensure that a token exists, and the token is requested, if the route requires a valid token
|
||||
$this->assertRequestToken($request, $route);
|
||||
// Ensure that sudo-mode is active, if the route requires it
|
||||
$this->assertSudoMode($request);
|
||||
|
||||
$targetIdentifier = $route->getOption('target');
|
||||
$target = $this->getCallableFromTarget($targetIdentifier);
|
||||
$arguments = [$request];
|
||||
try {
|
||||
return $target(...$arguments);
|
||||
} catch (MethodNotAllowedException $exception) {
|
||||
return $exception->createResponse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates HTTP `Referer` header (which is denied by client to be a custom
|
||||
* value) - attempts to ensure the value is given using a HTML client refresh.
|
||||
* see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
|
||||
*/
|
||||
protected function enforceReferrer(ServerRequestInterface $request, Route $route): ?ResponseInterface
|
||||
{
|
||||
if (!$this->features->isFeatureEnabled('security.backend.enforceReferrer')) {
|
||||
return null;
|
||||
}
|
||||
$referrerFlags = GeneralUtility::trimExplode(',', $route->getOption('referrer') ?? '', true);
|
||||
if (!in_array('required', $referrerFlags, true)) {
|
||||
return null;
|
||||
}
|
||||
return $this->referrerEnforcer->handle(
|
||||
$request,
|
||||
[
|
||||
'flags' => $referrerFlags,
|
||||
'subject' => $route->getPath(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the request token is valid. This is checked to see if the route is really
|
||||
* created by the same instance. Should be called for all routes in the backend except
|
||||
* for the ones that don't require a login.
|
||||
*
|
||||
* @see UriBuilder where the token is generated.
|
||||
*/
|
||||
protected function assertRequestToken(ServerRequestInterface $request, Route $route): void
|
||||
{
|
||||
if ($route->getOption('access') === 'public') {
|
||||
return;
|
||||
}
|
||||
$token = (string)($request->getParsedBody()['token'] ?? $request->getQueryParams()['token'] ?? '');
|
||||
if (empty($token)) {
|
||||
throw new MissingRequestTokenException(
|
||||
sprintf('Invalid request for route "%s"', $route->getPath()),
|
||||
1627905246
|
||||
);
|
||||
}
|
||||
$formProtection = $this->formProtectionFactory->createFromRequest($request);
|
||||
if (!$formProtection->validateToken($token, 'route', $route->getOption('_identifier'))) {
|
||||
throw new InvalidRequestTokenException(
|
||||
sprintf('Invalid request for route "%s"', $route->getPath()),
|
||||
1425389455
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that sudo mode verification was processed for this route before
|
||||
* and that it did not expire, yet. In case (re-)verification is required,
|
||||
* a corresponding `AccessClaim` is persisted in the user session storage,
|
||||
* and the process of showing the verification dialogs is initiated.
|
||||
*/
|
||||
protected function assertSudoMode(ServerRequestInterface $request): void
|
||||
{
|
||||
// #93160: [TASK] Do not require sudo mode in development context
|
||||
if (Environment::getContext()->isDevelopment()) {
|
||||
return;
|
||||
}
|
||||
/** @var ?Route $route */
|
||||
$route = $request->getAttribute('route');
|
||||
$settings = $route?->getOption('sudoMode') ?? null;
|
||||
if (!is_array($settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sudo mode settings for subject are fetched from the request again
|
||||
$subject = $this->factory->buildRouteAccessSubject($request);
|
||||
if ($this->storage->findGrantsBySubject($subject)) {
|
||||
return;
|
||||
}
|
||||
// reuse existing matching claim, or create a new one
|
||||
$claim = $this->storage->findClaimBySubject($subject)
|
||||
?? $this->factory->buildClaimForSubjectRequest($request, self::class, $subject);
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(new SudoModeRequiredEvent($claim));
|
||||
if ($event->isVerificationRequired()) {
|
||||
throw (new VerificationRequiredException(
|
||||
'Sudo Mode Confirmation Required',
|
||||
1605812020
|
||||
))->withClaim($claim);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user