TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<?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\Frontend\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Authentication\Mfa\MfaRequiredException;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\Authentication\FrontendBackendUserAuthentication;
|
||||
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
|
||||
|
||||
/**
|
||||
* This middleware authenticates a Backend User (be_user) (pre)-viewing a frontend page.
|
||||
*
|
||||
* This middleware also ensures that $GLOBALS['LANG'] is available, however it is possible that
|
||||
* a different middleware later-on might unset the BE_USER as he/she is not allowed to preview a certain
|
||||
* page due to rights management. As this can only happen once the page ID is resolved, this will happen
|
||||
* after the routing middleware.
|
||||
*/
|
||||
class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator
|
||||
{
|
||||
public function __construct(
|
||||
Context $context,
|
||||
protected readonly LanguageServiceFactory $languageServiceFactory
|
||||
) {
|
||||
parent::__construct($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a backend user authentication object, tries to authenticate a user
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// Initializing a possible logged-in Backend User
|
||||
// If the backend cookie is set,
|
||||
// we proceed and check if a backend user is logged in.
|
||||
$backendUserObject = null;
|
||||
if (isset($request->getCookieParams()[BackendUserAuthentication::getCookieName()])) {
|
||||
$backendUserObject = $this->initializeBackendUser($request);
|
||||
}
|
||||
$GLOBALS['BE_USER'] = $backendUserObject;
|
||||
// Load specific dependencies which are necessary for a valid Backend User
|
||||
// like $GLOBALS['LANG'] for labels in the language of the BE User (so AdminPanel works)
|
||||
if ($backendUserObject !== null) {
|
||||
$GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
|
||||
$this->setBackendUserAspect($GLOBALS['BE_USER']);
|
||||
if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)
|
||||
&& (strtolower($request->getServerParams()['HTTP_CACHE_CONTROL'] ?? '') === 'no-cache'
|
||||
|| strtolower($request->getServerParams()['HTTP_PRAGMA'] ?? '') === 'no-cache')
|
||||
) {
|
||||
// Detecting if shift-reload has been clicked to disable caching if so.
|
||||
// This is only done if a backend user is logged in to prevent DoS-attacks for "casual" requests.
|
||||
$cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
|
||||
$cacheInstruction->disableCache('EXT:frontend: Logged in backend user forced reload disabled cache.');
|
||||
$request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
|
||||
}
|
||||
}
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
// If, when building the response, the user is still available, then ensure that the headers are sent properly
|
||||
if ($this->context->getAspect('backend.user')->isLoggedIn()) {
|
||||
return $this->applyHeadersToResponse($response);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the backend user object and returns it if a valid backend user is found.
|
||||
*/
|
||||
protected function initializeBackendUser(ServerRequestInterface $request): ?FrontendBackendUserAuthentication
|
||||
{
|
||||
// New backend user object
|
||||
$backendUserObject = GeneralUtility::makeInstance(FrontendBackendUserAuthentication::class);
|
||||
try {
|
||||
$backendUserObject->start($request);
|
||||
} catch (MfaRequiredException $e) {
|
||||
// Do nothing, as the user is not fully authenticated - has not
|
||||
// passed required multi-factor authentication - via the backend.
|
||||
return null;
|
||||
}
|
||||
if (!empty($backendUserObject->user['uid'])) {
|
||||
$this->setBackendUserAspect($backendUserObject, (int)$backendUserObject->user['workspace_id']);
|
||||
$backendUserObject->fetchGroupData();
|
||||
}
|
||||
// Unset the user initialization if any setting / restriction applies
|
||||
if (!$this->isAuthenticated($backendUserObject, $request, $request->getAttribute('normalizedParams'))) {
|
||||
$backendUserObject = null;
|
||||
$this->setBackendUserAspect(null);
|
||||
}
|
||||
return $backendUserObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementing the access checks that the TYPO3 CMS bootstrap script does before a user is ever logged in.
|
||||
* Returns TRUE if access is OK
|
||||
*/
|
||||
protected function isAuthenticated(FrontendBackendUserAuthentication $user, ServerRequestInterface $request, NormalizedParams $normalizedParams): bool
|
||||
{
|
||||
// Check IP
|
||||
$ipMask = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList'] ?? '');
|
||||
if ($ipMask && !GeneralUtility::cmpIP($normalizedParams->getRemoteAddress(), $ipMask)) {
|
||||
return false;
|
||||
}
|
||||
// Check SSL (https)
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] && !$normalizedParams->isHttps()) {
|
||||
return false;
|
||||
}
|
||||
return $user->backendCheckLogin($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Frontend\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Handle cache timeout that is set in typoscript.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CacheTimeout implements MiddlewareInterface
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$response = $handler->handle($request);
|
||||
$config = $request->getAttribute('frontend.typoscript')?->getConfigArray() ?? [];
|
||||
if ($config['cache_clearAtMidnight'] ?? false) {
|
||||
// @todo: We should probably decide to deprecate or remove cache_clearAtMidnight
|
||||
// altogether since it is a flawed concept based on server timezone
|
||||
// "when is midnight?".
|
||||
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
|
||||
$timeOutTime = min($GLOBALS['EXEC_TIME'] + $cacheDataCollector->resolveLifetime(), PHP_INT_MAX);
|
||||
$midnightTime = mktime(0, 0, 0, (int)date('m', $timeOutTime), (int)date('d', $timeOutTime), (int)date('Y', $timeOutTime));
|
||||
// If the midnight time of the expire-day is greater than the current time,
|
||||
// we may set the timeOutTime to the new midnighttime.
|
||||
if ($midnightTime > $GLOBALS['EXEC_TIME']) {
|
||||
$cacheDataCollector->restrictMaximumLifetime($midnightTime - $GLOBALS['EXEC_TIME']);
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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\Frontend\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\Context\Context;
|
||||
|
||||
/**
|
||||
* Add content-length HTTP header to the response.
|
||||
*
|
||||
* Notice that all Content outside the length of the content-length header will be cut off!
|
||||
* Therefore, content of unknown length from later-on middlewares and if admin users are logged
|
||||
* in (admin panel might show...), we disable it!
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class ContentLengthResponseHeader implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(private Context $context) {}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$response = $handler->handle($request);
|
||||
$typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
|
||||
if (
|
||||
(!isset($typoScriptConfigArray['enableContentLengthHeader']) || $typoScriptConfigArray['enableContentLengthHeader'])
|
||||
&& !$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)
|
||||
&& !$this->context->getPropertyFromAspect('workspace', 'isOffline', false)
|
||||
) {
|
||||
$response = $response->withHeader('Content-Length', (string)$response->getBody()->getSize());
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?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\Frontend\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\CspConfigurationFactory;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
|
||||
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\Site\Entity\Site;
|
||||
|
||||
/**
|
||||
* 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 CspConfigurationFactory $cspConfigurationFactory,
|
||||
private ResponseService $responseService,
|
||||
private DirectiveHashCollection $directiveHashCollection,
|
||||
) {}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
return $this->applyContentSecurityPolicy($request, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Content-Security-Policy headers to an error response that bypassed
|
||||
* the normal middleware stack (e.g. responses from ErrorController).
|
||||
*/
|
||||
public function applyToResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
return $this->applyContentSecurityPolicy($request, $response);
|
||||
}
|
||||
|
||||
private function applyContentSecurityPolicy(ServerRequestInterface $request, ResponseInterface|RequestHandlerInterface $subject): ResponseInterface
|
||||
{
|
||||
$site = $request->getAttribute('site');
|
||||
$cspConfiguration = $site instanceof Site ? ($site->getConfiguration()['contentSecurityPolicies'] ?? []) : [];
|
||||
$dispositionMap = $this->cspConfigurationFactory->buildDispositionMap($cspConfiguration);
|
||||
$behavior = $this->cspConfigurationFactory->buildBehavior($cspConfiguration);
|
||||
// return early in case CSP shall not be used
|
||||
if ($dispositionMap->keys() === []) {
|
||||
return $subject instanceof RequestHandlerInterface ? $subject->handle($request) : $subject;
|
||||
}
|
||||
$scope = Scope::frontendSite($site);
|
||||
$nonce = $this->requestId->nonce;
|
||||
$policyBag = new PolicyBag($scope, $dispositionMap, $behavior, $nonce, $this->directiveHashCollection);
|
||||
// make sure, the nonce value is set before processing the remaining components
|
||||
$request = $request
|
||||
->withAttribute('nonce', $nonce)
|
||||
->withAttribute('csp.policyBag', $policyBag);
|
||||
$response = $subject instanceof RequestHandlerInterface ? $subject->handle($request) : $subject;
|
||||
if ($response->hasHeader('Content-Security-Policy') || $response->hasHeader('Content-Security-Policy-Report-Only')) {
|
||||
if ($subject instanceof RequestHandlerInterface) {
|
||||
$this->logger->info('Content-Security-Policy not enforced due to existence of custom header', [
|
||||
'scope' => (string)$scope,
|
||||
'uri' => (string)$request->getUri(),
|
||||
]);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
$processedEarlier = $policyBag->hasPolicies();
|
||||
$this->policyProvider->prepare($policyBag, $request, $response);
|
||||
foreach ($dispositionMap as $disposition => $dispositionConfiguration) {
|
||||
$policy = $policyBag->getPolicy($disposition);
|
||||
if ($policy->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
$response = $response->withHeader(
|
||||
$disposition->getHttpHeaderName(),
|
||||
$policy->compile($policyBag, $this->cache)
|
||||
);
|
||||
}
|
||||
if (!$processedEarlier && $policyBag->behavior->useNonce === false) {
|
||||
$response = $this->responseService->dropNonceFromHtmlResponse($response, $nonce);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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\Frontend\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;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ContentSecurityPolicyReporter extends AbstractContentSecurityPolicyReporter
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$site = $request->getAttribute('site');
|
||||
$scope = Scope::frontendSite($site);
|
||||
if ($this->targetsCspReportUri($scope, $request)) {
|
||||
$dispositionMap = $this->cspConfigurationFactory->buildDispositionMap(
|
||||
$site instanceof Site ? ($site->getConfiguration()['contentSecurityPolicies'] ?? []) : []
|
||||
);
|
||||
// find at least one configured reporting endpoint for the current request
|
||||
foreach ($dispositionMap->values() as $dispositionConfiguration) {
|
||||
if ($this->isCspReport($scope, $request, $dispositionConfiguration)) {
|
||||
$isCspReport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!($isCspReport ?? false)) {
|
||||
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,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\Frontend\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\DispatcherInterface;
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
|
||||
/**
|
||||
* Lightweight alternative to regular frontend requests; used when $_GET[eID] is set.
|
||||
* In the future, logic from the EidUtility will be moved to this class, however in most cases
|
||||
* a custom PSR-15 middleware will be better suited for whatever job the eID functionality does currently.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class EidHandler implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected DispatcherInterface $dispatcher
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Dispatches the request to the corresponding eID class or eID script
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$eID = $request->getParsedBody()['eID'] ?? $request->getQueryParams()['eID'] ?? null;
|
||||
|
||||
if ($eID === null) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
// Remove any output produced until now
|
||||
ob_clean();
|
||||
|
||||
if (!is_string($eID)) {
|
||||
return (new Response())->withStatus(400, 'Invalid eID');
|
||||
}
|
||||
|
||||
$target = $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include'][$eID] ?? null;
|
||||
if (empty($target)) {
|
||||
return (new Response())->withStatus(404, 'eID not registered');
|
||||
}
|
||||
|
||||
$request = $request->withAttribute('target', $target);
|
||||
return $this->dispatcher->dispatch($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?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\Frontend\Middleware;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
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\RateLimiter\LimiterInterface;
|
||||
use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
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;
|
||||
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
|
||||
|
||||
/**
|
||||
* This middleware authenticates a Frontend User (fe_users).
|
||||
*/
|
||||
readonly class FrontendUserAuthenticator implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected Context $context,
|
||||
protected RateLimiterFactoryInterface $rateLimiterFactory,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a frontend user authentication object, tries to authenticate a user and stores
|
||||
* it in the current request as attribute.
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class);
|
||||
|
||||
// Rate Limiting
|
||||
$rateLimiter = $this->ensureLoginRateLimit($frontendUser, $request);
|
||||
|
||||
// Authenticate now
|
||||
$frontendUser->start($request);
|
||||
// no matter if we have an active user we try to fetch matching groups which can
|
||||
// be set without an user (simulation for instance!)
|
||||
$frontendUser->fetchGroupData($request);
|
||||
|
||||
// Register the frontend user as aspect and within the request
|
||||
$this->context->setAspect('frontend.user', $frontendUser->createUserAspect());
|
||||
$request = $request->withAttribute('frontend.user', $frontendUser);
|
||||
|
||||
if ($this->context->getAspect('frontend.user')->isLoggedIn() && $rateLimiter) {
|
||||
$rateLimiter->reset();
|
||||
$this->eventDispatcher->dispatch(new AfterUserLoggedInEvent($frontendUser, $request));
|
||||
}
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
// Store session data for fe_users
|
||||
$frontendUser->storeSessionData();
|
||||
$response = $frontendUser->appendCookieToResponse($response, $request->getAttribute('normalizedParams'));
|
||||
// Collect garbage in Frontend requests, which aren't fully cacheable (e.g. with cookies)
|
||||
if ($response->hasHeader('Set-Cookie')) {
|
||||
$this->sessionGarbageCollection();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collection for fe_sessions (with a probability)
|
||||
*/
|
||||
protected function sessionGarbageCollection(): void
|
||||
{
|
||||
UserSessionManager::create('FE')->collectGarbage();
|
||||
}
|
||||
|
||||
protected function ensureLoginRateLimit(FrontendUserAuthentication $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',
|
||||
1616175847
|
||||
);
|
||||
}
|
||||
return $loginRateLimiter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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\Frontend\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\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
|
||||
/**
|
||||
* Checks whether the whole Frontend should be put into "Page unavailable mode"
|
||||
* unless the devIPMask matches the current visitor's IP.
|
||||
*
|
||||
* The global setting $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_force']
|
||||
* is used for turning on the maintenance mode.
|
||||
*/
|
||||
class MaintenanceMode implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Calls the "unavailableAction" of the error controller if the system is in maintenance mode.
|
||||
* This only applies if the REMOTE_ADDR does not match the devIpMask
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_force']
|
||||
&& !GeneralUtility::cmpIP(
|
||||
$request->getAttribute('normalizedParams')->getRemoteAddress(),
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
|
||||
)
|
||||
) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($request, 'This page is temporarily unavailable.');
|
||||
}
|
||||
// Continue the regular stack if no maintenance mode is active
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?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\Frontend\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\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* This middleware validates given request parameters against the common "cHash" functionality.
|
||||
*/
|
||||
readonly class PageArgumentValidator implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CacheHashCalculator $cacheHashCalculator,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the &cHash parameter against the other $queryParameters / GET parameters
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
|
||||
$request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
|
||||
$pageNotFoundOnValidationError = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] ?? true);
|
||||
$pageArguments = $request->getAttribute('routing');
|
||||
if (!($pageArguments instanceof PageArguments)) {
|
||||
// Page Arguments must be set in order to validate. This middleware only works if PageArguments
|
||||
// is available, and is usually combined with the Page Resolver middleware
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'Page Arguments could not be resolved',
|
||||
['code' => PageAccessFailureReasons::INVALID_PAGE_ARGUMENTS]
|
||||
);
|
||||
}
|
||||
if (!($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter'] ?? true)
|
||||
&& ($pageArguments->getArguments()['no_cache'] ?? $request->getParsedBody()['no_cache'] ?? false)
|
||||
) {
|
||||
$cacheInstruction->disableCache('EXT:frontend: Caching disabled by no_cache query argument.');
|
||||
}
|
||||
if (!$cacheInstruction->isCachingAllowed() && !$pageNotFoundOnValidationError) {
|
||||
// No need to test anything if caching was already disabled.
|
||||
return $handler->handle($request);
|
||||
}
|
||||
// Evaluate the cache hash parameter or dynamic arguments when coming from a Site-based routing
|
||||
$cHash = '';
|
||||
if (isset($pageArguments->getArguments()['cHash']) && is_scalar($pageArguments->getArguments()['cHash'])) {
|
||||
$cHash = (string)($pageArguments->getArguments()['cHash']);
|
||||
}
|
||||
$queryParams = $pageArguments->getDynamicArguments();
|
||||
if ($cHash !== '' || !empty($queryParams)) {
|
||||
$relevantParametersForCacheHashArgument = $this->getRelevantParametersForCacheHashCalculation($pageArguments);
|
||||
if ($cHash !== '') {
|
||||
if (empty($relevantParametersForCacheHashArgument)) {
|
||||
// cHash was given, but nothing to be calculated, so let's do a redirect to the current page but without the cHash
|
||||
$this->logger->notice('The incoming cHash "{hash}" is given but not needed. cHash is unset', ['hash' => $cHash]);
|
||||
$uri = $request->getUri();
|
||||
unset($queryParams['cHash']);
|
||||
$uri = $uri->withQuery(HttpUtility::buildQueryString($queryParams));
|
||||
return new RedirectResponse($uri, 308);
|
||||
}
|
||||
if (!$this->evaluateCacheHashParameter($cacheInstruction, $cHash, $relevantParametersForCacheHashArgument, $pageNotFoundOnValidationError)) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'Request parameters could not be validated (&cHash comparison failed)',
|
||||
['code' => PageAccessFailureReasons::CACHEHASH_COMPARISON_FAILED]
|
||||
);
|
||||
}
|
||||
// No cHash given but was required
|
||||
} elseif (!$this->evaluatePageArgumentsWithoutCacheHash($cacheInstruction, $pageArguments, $pageNotFoundOnValidationError)) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'Request parameters could not be validated (&cHash empty)',
|
||||
['code' => PageAccessFailureReasons::CACHEHASH_EMPTY]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out the arguments that are necessary for calculating cHash
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function getRelevantParametersForCacheHashCalculation(PageArguments $pageArguments): array
|
||||
{
|
||||
$queryParams = $pageArguments->getDynamicArguments();
|
||||
$queryParams['id'] = $pageArguments->getPageId();
|
||||
return $this->cacheHashCalculator->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a hash string based on additional parameters in the url.
|
||||
* This is used to cache pages with more parameters than just id and type.
|
||||
*
|
||||
* @param string $cHash the chash to check
|
||||
* @param array<string, string> $relevantParameters GET parameters necessary for cHash calculation
|
||||
* @param bool $pageNotFoundOnCacheHashError see $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']
|
||||
* @return bool if false, then a PageNotFound response is triggered
|
||||
*/
|
||||
protected function evaluateCacheHashParameter(CacheInstruction $cacheInstruction, string $cHash, array $relevantParameters, bool $pageNotFoundOnCacheHashError): bool
|
||||
{
|
||||
$calculatedCacheHash = $this->cacheHashCalculator->calculateCacheHash($relevantParameters);
|
||||
if (hash_equals($calculatedCacheHash, $cHash)) {
|
||||
return true;
|
||||
}
|
||||
// Early return to trigger the error controller
|
||||
if ($pageNotFoundOnCacheHashError) {
|
||||
return false;
|
||||
}
|
||||
// Caching is disabled now (but no 404)
|
||||
$cacheInstruction->disableCache('EXT:frontend: Incoming cHash "' . $cHash . '" and calculated cHash "' . $calculatedCacheHash . '" did not match.'
|
||||
. ' The field list used was "' . implode(',', array_keys($relevantParameters)) . '". Caching is disabled.');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* No cHash is set but there are query parameters, check if that is correct
|
||||
*
|
||||
* Should only be called if NO cHash parameter is given.
|
||||
*
|
||||
* @param array<string, string|array> $dynamicArguments
|
||||
*/
|
||||
protected function evaluateQueryParametersWithoutCacheHash(CacheInstruction $cacheInstruction, array $dynamicArguments, bool $pageNotFoundOnCacheHashError): bool
|
||||
{
|
||||
if (!$this->cacheHashCalculator->doParametersRequireCacheHash(HttpUtility::buildQueryString($dynamicArguments))) {
|
||||
return true;
|
||||
}
|
||||
// cHash is required, but not given, so trigger a 404
|
||||
if ($pageNotFoundOnCacheHashError) {
|
||||
return false;
|
||||
}
|
||||
// Caching is disabled now (but no 404)
|
||||
$cacheInstruction->disableCache('EXT:frontend: No cHash query argument was sent for GET vars though required. Caching is disabled.');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* No cHash is set but there are query parameters, then calculate a possible cHash from the given
|
||||
* query parameters and see if a cHash is returned (similar to comparing this).
|
||||
*
|
||||
* Is only called if NO cHash parameter is given.
|
||||
*/
|
||||
protected function evaluatePageArgumentsWithoutCacheHash(CacheInstruction $cacheInstruction, PageArguments $pageArguments, bool $pageNotFoundOnCacheHashError): bool
|
||||
{
|
||||
// legacy behaviour
|
||||
if (!($GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['enforceValidation'] ?? false)) {
|
||||
return $this->evaluateQueryParametersWithoutCacheHash($cacheInstruction, $pageArguments->getDynamicArguments(), $pageNotFoundOnCacheHashError);
|
||||
}
|
||||
$relevantParameters = $this->getRelevantParametersForCacheHashCalculation($pageArguments);
|
||||
// There are parameters that would be needed for the current page, but no cHash is given.
|
||||
// Thus, a "page not found" error is thrown - as configured via "pageNotFoundOnCHashError".
|
||||
if (!empty($relevantParameters) && $pageNotFoundOnCacheHashError) {
|
||||
return false;
|
||||
}
|
||||
// There are no parameters that require a cHash.
|
||||
// We end up here when the site was called with an `id` param, e.g. https://example.org/index?id=123.
|
||||
// Avoid disabling caches in this case.
|
||||
if (empty($relevantParameters)) {
|
||||
return true;
|
||||
}
|
||||
// Caching is disabled now (but no 404)
|
||||
$cacheInstruction->disableCache('EXT:frontend: No cHash query argument was sent for given query parameters. Caching is disabled');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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\Frontend\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\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\Routing\RouteNotFoundException;
|
||||
use TYPO3\CMS\Core\Routing\SiteRouteResult;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* Resolve the page ID based on TYPO3's routing functionality configured in a site.
|
||||
*
|
||||
* Processes the page ID, page type (typeNum) and other parameters built from queryArguments and routeParameters.
|
||||
*
|
||||
* However, if there is a backend user logged in and he has NO access to this page (and the page is hidden),
|
||||
* then the ID is determined again and the backend user is not considered for the rest of the frontend request.
|
||||
*/
|
||||
class PageResolver implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Resolve the page ID
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$site = $request->getAttribute('site', null);
|
||||
|
||||
if (!$site instanceof Site) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'No site configuration found.',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
}
|
||||
|
||||
/** @var SiteRouteResult|null $previousResult */
|
||||
$previousResult = $request->getAttribute('routing', null);
|
||||
if (!$previousResult) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
}
|
||||
|
||||
// Check for the route arguments or Query Parameter ID
|
||||
try {
|
||||
/** @var PageArguments $pageArguments */
|
||||
$pageArguments = $site->getRouter()->matchRequest($request, $previousResult);
|
||||
$request = $request->withAttribute('routing', $pageArguments);
|
||||
} catch (RouteNotFoundException $e) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
}
|
||||
|
||||
if (!$pageArguments->getPageId()) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
}
|
||||
|
||||
// stop in case arguments are dirty (=defined twice in route and GET query parameters)
|
||||
if ($pageArguments->areDirty()) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested URL is not distinct',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
}
|
||||
|
||||
// merge the PageArguments with the request query parameters
|
||||
$queryParams = array_replace_recursive($request->getQueryParams(), $pageArguments->getArguments());
|
||||
$request = $request->withQueryParams($queryParams);
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
<?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\Frontend\Middleware;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
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\CacheTag;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Localization\Locale;
|
||||
use TYPO3\CMS\Core\Localization\Locales;
|
||||
use TYPO3\CMS\Core\Locking\ResourceMutex;
|
||||
use TYPO3\CMS\Core\Page\AssetCollector;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
|
||||
use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
|
||||
use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
|
||||
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
|
||||
use TYPO3\CMS\Frontend\Cache\MetaDataState;
|
||||
use TYPO3\CMS\Frontend\ContentObject\RegisterStack;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent;
|
||||
use TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent;
|
||||
use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent;
|
||||
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
use TYPO3\CMS\Frontend\Page\PageInformationCreationFailedException;
|
||||
use TYPO3\CMS\Frontend\Page\PageInformationFactory;
|
||||
use TYPO3\CMS\Frontend\Page\PageParts;
|
||||
use TYPO3\CMS\Frontend\Response\ResponseData;
|
||||
|
||||
/**
|
||||
* This important middleware prepares a lot of the heavy lifting.
|
||||
*
|
||||
* It is all about page determination, caching, locking, various request attributes and
|
||||
* TypoScript calculation. All these aspects depends on each other, this middleware determines
|
||||
* if the requested page has to be fully or partially rendered, determines cache state and sets
|
||||
* up system by adding request attributes and a setting up a couple of singletons.
|
||||
*
|
||||
* @internal this middleware might get removed later.
|
||||
*/
|
||||
final readonly class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private FrontendTypoScriptFactory $frontendTypoScriptFactory,
|
||||
#[Autowire(service: 'cache.typoscript')]
|
||||
private PhpFrontend $typoScriptCache,
|
||||
#[Autowire(service: 'cache.pages')]
|
||||
private FrontendInterface $pageCache,
|
||||
private ResourceMutex $lock,
|
||||
private Context $context,
|
||||
private LoggerInterface $logger,
|
||||
private ErrorController $errorController,
|
||||
private TimeTracker $timeTracker,
|
||||
private PageInformationFactory $pageInformationFactory,
|
||||
// injecting this central stateful singleton. this is usually a smell, but ok in this case as exception.
|
||||
private PageRenderer $pageRenderer,
|
||||
private Locales $locales,
|
||||
) {}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// Make sure frontend.preview aspect is given from now on
|
||||
if (!$this->context->hasAspect('frontend.preview')) {
|
||||
$this->context->setAspect('frontend.preview', new PreviewAspect());
|
||||
}
|
||||
|
||||
// Verify crucial request attributes exist at this point
|
||||
if (!$request->getAttribute('routing') instanceof PageArguments || !$request->getAttribute('normalizedParams') instanceof NormalizedParams) {
|
||||
throw new \RuntimeException('Request attribute "routing" or "normalizedParams" not found. Error in previous middleware.', 1703150865);
|
||||
}
|
||||
|
||||
$site = $request->getAttribute('site');
|
||||
// Cache instruction attribute may have been set by previous middlewares
|
||||
$cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction());
|
||||
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
|
||||
$language = $request->getAttribute('language') ?? $site->getDefaultLanguage();
|
||||
$routing = $request->getAttribute('routing');
|
||||
|
||||
// Add the (internal!) response data "collector" attribute to allow single content elements
|
||||
// to register HTTP header data. We do not check for attribute existence here, since
|
||||
// middlewares *before* could just add their own headers directly.
|
||||
$request = $request->withAttribute('frontend.response.data', new ResponseData());
|
||||
|
||||
if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) {
|
||||
// Disable cache if this is a preview
|
||||
$cacheInstruction->disableCache('EXT:frontend: Disabled cache due to enabled frontend.preview aspect isPreview.');
|
||||
}
|
||||
// Make sure cache instruction attribute is always set from now on
|
||||
$request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
|
||||
// Did above code or some previous call disable cache?
|
||||
$isCachingAllowed = $cacheInstruction->isCachingAllowed();
|
||||
|
||||
// Create and add PageInformation
|
||||
try {
|
||||
$this->timeTracker->push('Create PageInformation');
|
||||
$pageInformation = $this->pageInformationFactory->create($request);
|
||||
} catch (PageInformationCreationFailedException $exception) {
|
||||
return $exception->getResponse();
|
||||
} finally {
|
||||
$this->timeTracker->pull();
|
||||
}
|
||||
$request = $request->withAttribute('frontend.page.information', $pageInformation);
|
||||
$sysTemplateRows = $pageInformation->getSysTemplateRows();
|
||||
|
||||
// Init and add register and page parts
|
||||
$request = $request->withAttribute('frontend.register.stack', new RegisterStack());
|
||||
$pageParts = new PageParts();
|
||||
// Init "last changed" with "tstamp" of the page record, or SYS_LASTCHANGED if it's younger
|
||||
$lastChanged = (int)$pageInformation->getPageRecord()['tstamp'];
|
||||
if ($lastChanged < (int)$pageInformation->getPageRecord()['SYS_LASTCHANGED']) {
|
||||
$lastChanged = (int)$pageInformation->getPageRecord()['SYS_LASTCHANGED'];
|
||||
}
|
||||
$pageParts->setLastChanged($lastChanged);
|
||||
$request = $request->withAttribute('frontend.page.parts', $pageParts);
|
||||
|
||||
// Create FrontendTypoScript with essential info for page cache identifier
|
||||
$conditionMatcherVariables = $this->prepareConditionMatcherVariables($request);
|
||||
$frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions(
|
||||
$site,
|
||||
$sysTemplateRows,
|
||||
$conditionMatcherVariables,
|
||||
$isCachingAllowed ? $this->typoScriptCache : null,
|
||||
);
|
||||
|
||||
// Set up cache relevant information
|
||||
$isUsingPageCacheAllowed = $this->eventDispatcher->dispatch(new ShouldUseCachedPageDataIfAvailableEvent($request, $isCachingAllowed))->shouldUseCachedPageData();
|
||||
$pageCacheIdentifier = $this->createPageCacheIdentifier($request, $frontendTypoScript);
|
||||
$cacheDataCollector->setPageCacheIdentifier($pageCacheIdentifier);
|
||||
|
||||
// Get page cache row or lock rendering
|
||||
$pageCacheRow = null;
|
||||
if (!$isUsingPageCacheAllowed) {
|
||||
// Caching not allowed. We'll rebuild the page. Lock this.
|
||||
$this->lock->acquireLock('pages', $pageCacheIdentifier);
|
||||
} else {
|
||||
// Try to get a page cache row.
|
||||
$pageCacheRow = $this->pageCache->get($pageCacheIdentifier);
|
||||
if (!is_array($pageCacheRow)) {
|
||||
// Nothing in the cache, we acquire an exclusive lock now.
|
||||
// There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll
|
||||
// "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In
|
||||
// this case, the below call will wait until the lock is released again. The other process then probably wrote
|
||||
// a page cache entry, which we can use.
|
||||
// To handle the second case - if our process had to wait for another one creating the content for us - we
|
||||
// simply query the page cache again to see if there is a page cache now.
|
||||
$hadToWaitForLock = $this->lock->acquireLock('pages', $pageCacheIdentifier);
|
||||
// From this point on we're the only one working on that page.
|
||||
if ($hadToWaitForLock) {
|
||||
// Query the cache again to see if the data is there meanwhile: We did not get the lock
|
||||
// immediately, chances are high the other process created a page cache for us.
|
||||
// There is a small chance the other process actually pageCache->set() the content,
|
||||
// but pageCache->get() still returns false, for instance when a database returned "done"
|
||||
// for the INSERT, but SELECT still does not return the new row - may happen in multi-head
|
||||
// DB instances, and with some other distributed cache backends as well. The worst that
|
||||
// can happen here is the page generation is done too often, which we accept as trade-off.
|
||||
$pageCacheRow = $this->pageCache->get($pageCacheIdentifier);
|
||||
if (is_array($pageCacheRow)) {
|
||||
// Got cache row, some other process did the work for us, release our lock again.
|
||||
$this->lock->releaseLock('pages');
|
||||
}
|
||||
}
|
||||
// Keep lock, we are the one generating the page now and fill cache.
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($pageCacheRow)) {
|
||||
// Got page from cache. Set up system state with it.
|
||||
$pageParts->setPageContentWasLoadedFromCache();
|
||||
$pageParts->setPageCacheGeneratedTimestamp($pageCacheRow['pageCacheGeneratedTimestamp']);
|
||||
$pageParts->setPageCacheExpireTimestamp($pageCacheRow['pageCacheExpireTimestamp']);
|
||||
foreach ($pageCacheRow['INTincScript'] as $intIncScript) {
|
||||
$pageParts->addNotCachedContentElement($intIncScript);
|
||||
}
|
||||
$pageParts->setPageTitle($pageCacheRow['pageTitleCache']);
|
||||
$pageParts->setContent($pageCacheRow['content']);
|
||||
$pageParts->setHttpContentType($pageCacheRow['contentType']);
|
||||
$pageParts->setPageRendererSubstitutionHash($pageCacheRow['pageRendererSubstitutionHash']);
|
||||
if ($pageCacheRow['pageRendererState'] ?? false) {
|
||||
$pageRendererState = unserialize($pageCacheRow['pageRendererState'], ['allowed_classes' => [Locale::class]]);
|
||||
$this->pageRenderer->updateState($pageRendererState);
|
||||
}
|
||||
if ($pageCacheRow['assetCollectorState'] ?? false) {
|
||||
$assetCollectorState = unserialize($pageCacheRow['assetCollectorState'], ['allowed_classes' => false]);
|
||||
$assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
|
||||
$assetCollector->updateState($assetCollectorState);
|
||||
}
|
||||
// Restore the current tags and add them to the CacheTageCollector
|
||||
$lifetime = $pageParts->getPageCacheExpireTimestamp() - $GLOBALS['EXEC_TIME'];
|
||||
$cacheTags = array_map(fn(string $cacheTag) => new CacheTag($cacheTag, $lifetime), $pageCacheRow['cacheTags'] ?? []);
|
||||
$cacheDataCollector->addCacheTags(...$cacheTags);
|
||||
// Restore meta-data state
|
||||
if (is_array($pageCacheRow['metaDataState'] ?? null)) {
|
||||
GeneralUtility::makeInstance(MetaDataState::class)->updateState($pageCacheRow['metaDataState']);
|
||||
}
|
||||
} else {
|
||||
// Init FE PageRenderer defaults when this page needs to be generated
|
||||
if ($language->hasCustomTypo3Language()) {
|
||||
$locale = $this->locales->createLocale($language->getTypo3Language());
|
||||
} else {
|
||||
// The createLocale() call is needed in order to resolve dependencies from the Locales class
|
||||
$locale = $this->locales->createLocale((string)$language->getLocale());
|
||||
}
|
||||
$this->pageRenderer->setLanguage($locale, $request);
|
||||
$pageParts->setPageRendererSubstitutionHash(md5(StringUtility::getUniqueId()));
|
||||
$pageParts->setPageCacheGeneratedTimestamp($GLOBALS['EXEC_TIME']);
|
||||
}
|
||||
// Processing of page cache row done. Do not use this variable anymore.
|
||||
unset($pageCacheRow);
|
||||
|
||||
try {
|
||||
$needsFullSetup = !$pageParts->hasPageContentBeenLoadedFromCache() || $pageParts->hasNotCachedContentElements();
|
||||
$pageType = $routing->getPageType();
|
||||
$frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(
|
||||
$needsFullSetup,
|
||||
$frontendTypoScript,
|
||||
$site,
|
||||
$sysTemplateRows,
|
||||
$conditionMatcherVariables,
|
||||
$pageType,
|
||||
$isCachingAllowed ? $this->typoScriptCache : null,
|
||||
$request,
|
||||
);
|
||||
if ($needsFullSetup && !$frontendTypoScript->hasPage()) {
|
||||
$this->logger->error('No page configured for type={type}. There is no TypoScript object of type PAGE with typeNum={type}.', ['type' => $pageType]);
|
||||
return $this->errorController->internalErrorAction(
|
||||
$request,
|
||||
'No page configured for type=' . $pageType . '.',
|
||||
['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
|
||||
);
|
||||
}
|
||||
$setupConfigAst = $frontendTypoScript->getConfigTree();
|
||||
if ($setupConfigAst->getChildByName('no_cache')?->getValue()) {
|
||||
// Disable cache if config.no_cache is set!
|
||||
$cacheInstruction = $request->getAttribute('frontend.cache.instruction');
|
||||
$cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"');
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new AfterTypoScriptDeterminedEvent($frontendTypoScript));
|
||||
|
||||
$request = $request->withAttribute('frontend.typoscript', $frontendTypoScript);
|
||||
|
||||
return $handler->handle($request);
|
||||
} finally {
|
||||
// Whatever happens in a middleware below, this finally is called, even when exceptions
|
||||
// are raised by a lower one. This ensures locks are released no matter what.
|
||||
$this->lock->releaseLock('pages');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data available in TypoScript "condition" matching.
|
||||
*/
|
||||
private function prepareConditionMatcherVariables(ServerRequestInterface $request): array
|
||||
{
|
||||
$pageInformation = $request->getAttribute('frontend.page.information');
|
||||
$topDownRootLine = $pageInformation->getRootLine();
|
||||
$localRootline = $pageInformation->getLocalRootLine();
|
||||
ksort($topDownRootLine);
|
||||
return [
|
||||
'request' => $request,
|
||||
'pageId' => $pageInformation->getId(),
|
||||
'page' => $pageInformation->getPageRecord(),
|
||||
'fullRootLine' => $topDownRootLine,
|
||||
'localRootLine' => $localRootline,
|
||||
'site' => $request->getAttribute('site'),
|
||||
'siteLanguage' => $request->getAttribute('language'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates a hash used as page cache entry identifier and as page generation lock.
|
||||
* When multiple requests try to render the same page that will result in the same page cache entry,
|
||||
* this lock allows creation by one request which typically puts the result into page cache, while
|
||||
* the other requests wait until this finished and re-use the result.
|
||||
*/
|
||||
private function createPageCacheIdentifier(ServerRequestInterface $request, FrontendTypoScript $frontendTypoScript): string
|
||||
{
|
||||
$pageInformation = $request->getAttribute('frontend.page.information');
|
||||
$pageId = $pageInformation->getId();
|
||||
$pageArguments = $request->getAttribute('routing');
|
||||
$site = $request->getAttribute('site');
|
||||
|
||||
$dynamicArguments = [];
|
||||
$queryParams = $pageArguments->getDynamicArguments();
|
||||
if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) {
|
||||
// Fetch arguments relevant for creating the page cache identifier from the PageArguments object.
|
||||
// Excluded parameters are not taken into account when calculating the hash base.
|
||||
$queryParams['id'] = $pageArguments->getPageId();
|
||||
// @todo: Make CacheHashCalculator and CacheHashConfiguration stateless and get it injected.
|
||||
$dynamicArguments = GeneralUtility::makeInstance(CacheHashCalculator::class)
|
||||
->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
|
||||
}
|
||||
|
||||
$pageCacheIdentifierParameters = [
|
||||
'projectPath' => Environment::getProjectPath(),
|
||||
'id' => $pageId,
|
||||
'type' => $pageArguments->getPageType(),
|
||||
'groupIds' => implode(',', $this->context->getAspect('frontend.user')->getGroupIds()),
|
||||
'MP' => $pageInformation->getMountPoint(),
|
||||
'site' => $site->getIdentifier(),
|
||||
// Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
|
||||
// is not cached properly as we don't have any language-specific conditions anymore
|
||||
'siteBase' => (string)$request->getAttribute('language', $site->getDefaultLanguage())->getBase(),
|
||||
// additional variation trigger for static routes
|
||||
'staticRouteArguments' => $pageArguments->getStaticArguments(),
|
||||
// dynamic route arguments (if route was resolved)
|
||||
'dynamicArguments' => $dynamicArguments,
|
||||
'sysTemplateRows' => $pageInformation->getSysTemplateRows(),
|
||||
'constantConditionList' => $frontendTypoScript->getSettingsConditionList(),
|
||||
'setupConditionList' => $frontendTypoScript->getSetupConditionList(),
|
||||
];
|
||||
$pageCacheIdentifierParameters = $this->eventDispatcher
|
||||
->dispatch(new BeforePageCacheIdentifierIsHashedEvent($request, $pageCacheIdentifierParameters))
|
||||
->getPageCacheIdentifierParameters();
|
||||
|
||||
return $pageId . '_' . hash('xxh3', serialize($pageCacheIdentifierParameters));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?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\Frontend\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\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
|
||||
use TYPO3\CMS\Core\Context\VisibilityAspect;
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFactory;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\RootlineUtility;
|
||||
use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* Middleware for handling preview settings
|
||||
* used when simulating / previewing pages or content through query params when
|
||||
* previewing access or time restricted content via for example backend preview links
|
||||
*/
|
||||
readonly class PreviewSimulator implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected Context $context,
|
||||
protected PageRepository $pageRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Evaluates preview settings if a backend user is logged in
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$isLoggedIn = $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
|
||||
$isOfflineWorkspace = $this->context->getPropertyFromAspect('workspace', 'isOffline', false);
|
||||
// When previewing a workspace with the preview link, the PreviewUserAuthentication is NOT marked as
|
||||
// "isLoggedIn" as it does not have a valid user ID. For this reason, we also check if the Workspace is offline. See WorkspacePreview middleware
|
||||
if ($isLoggedIn || $isOfflineWorkspace) {
|
||||
$pageArguments = $request->getAttribute('routing', null);
|
||||
if (!$pageArguments instanceof PageArguments) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'Page Arguments could not be resolved',
|
||||
['code' => PageAccessFailureReasons::INVALID_PAGE_ARGUMENTS]
|
||||
);
|
||||
}
|
||||
$visibilityAspect = $this->context->getAspect('visibility');
|
||||
// The preview flag is set if the current page turns out to be hidden
|
||||
$showHiddenPages = $this->checkIfPageIsHidden($pageArguments->getPageId(), $request);
|
||||
$rootlineRequiresPreviewFlag = $this->checkIfRootlineRequiresPreview($pageArguments->getPageId());
|
||||
$simulatingDate = $this->simulateDate($request);
|
||||
$simulatingGroup = $this->simulateUserGroup($request);
|
||||
$showHiddenRecords = $visibilityAspect->includeHidden();
|
||||
$isPreview = $simulatingDate || $simulatingGroup || $showHiddenRecords || $showHiddenPages || $isOfflineWorkspace || $rootlineRequiresPreviewFlag;
|
||||
if ($this->context->hasAspect('frontend.preview')) {
|
||||
/** @var PreviewAspect $previewAspect */
|
||||
$previewAspect = $this->context->getAspect('frontend.preview');
|
||||
$isPreview = $previewAspect->isPreview() || $isPreview;
|
||||
}
|
||||
$this->context->setAspect('frontend.preview', new PreviewAspect($isPreview));
|
||||
|
||||
if ($showHiddenPages || $rootlineRequiresPreviewFlag) {
|
||||
$newAspect = new VisibilityAspect(true, $visibilityAspect->includeHiddenContent(), $visibilityAspect->includeDeletedRecords(), $visibilityAspect->includeScheduledRecords());
|
||||
$this->context->setAspect('visibility', $newAspect);
|
||||
}
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if the "extendToSubpages" flag was set on any of the previous ancestor pages,
|
||||
* but be sure to not check for the current page itself.
|
||||
*/
|
||||
protected function checkIfRootlineRequiresPreview(int $pageId): bool
|
||||
{
|
||||
$rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageId, '', $this->context);
|
||||
$groupRestricted = false;
|
||||
$timeRestricted = false;
|
||||
$hidden = false;
|
||||
try {
|
||||
$rootLine = $rootlineUtility->get();
|
||||
|
||||
// Remove the current page from the rootline
|
||||
array_shift($rootLine);
|
||||
foreach ($rootLine as $page) {
|
||||
// Skip root node and pages which do not define extendToSubpages
|
||||
if ((int)($page['uid'] ?? 0) === 0 || !(bool)($page['extendToSubpages'] ?? false)) {
|
||||
continue;
|
||||
}
|
||||
$groupRestricted = (bool)(string)($page['fe_group'] ?? '');
|
||||
$timeRestricted = (int)($page['starttime'] ?? 0) || (int)($page['endtime'] ?? 0);
|
||||
$hidden = (int)($page['hidden'] ?? 0);
|
||||
// Stop as soon as a page in the rootline has extendToSubpages set
|
||||
break;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
// if the rootline cannot be resolved (404 because of delete placeholder in workspaces for example)
|
||||
// we do not want to fail here but rather continue handling the request to trigger the middleware 404 handling
|
||||
}
|
||||
return $groupRestricted || $timeRestricted || $hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is hidden in the active workspace + language setup.
|
||||
*/
|
||||
protected function checkIfPageIsHidden(int $pageId, ServerRequestInterface $request): bool
|
||||
{
|
||||
$site = $request->getAttribute('site', null);
|
||||
// always check both the page in the requested language and the page in the default language, as due to the
|
||||
// overlay handling, a hidden default page will require setting the preview flag to allow previewing of the
|
||||
// translation
|
||||
$languageAspectFromRequest = LanguageAspectFactory::createFromSiteLanguage($request->getAttribute('language', $site->getDefaultLanguage()));
|
||||
$pageIsHidden = $this->pageRepository->checkIfPageIsHidden($pageId, $languageAspectFromRequest);
|
||||
|
||||
if ($languageAspectFromRequest->getId() > 0) {
|
||||
$pageIsHidden = $pageIsHidden || $this->pageRepository->checkIfPageIsHidden(
|
||||
$pageId,
|
||||
LanguageAspectFactory::createFromSiteLanguage($site->getDefaultLanguage())
|
||||
);
|
||||
}
|
||||
return $pageIsHidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate dates for preview functionality
|
||||
* When previewing a time restricted page from the backend, the parameter ADMCMD_simTime it added containing
|
||||
* a timestamp with the time to preview. The globals 'SIM_EXEC_TIME' and 'SIM_ACCESS_TIME' and the 'DateTimeAspect'
|
||||
* are used to simulate rendering at that point in time.
|
||||
* Ideally the global access is removed in future versions.
|
||||
* This functionality needs to be loaded after BackendAuthenticator as it is only relevant for
|
||||
* logged in backend users and needs to be done before any page resolving starts.
|
||||
*/
|
||||
protected function simulateDate(ServerRequestInterface $request): bool
|
||||
{
|
||||
$queryTime = (int)($request->getQueryParams()['ADMCMD_simTime'] ?? 0);
|
||||
if ($queryTime === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$GLOBALS['SIM_EXEC_TIME'] = $queryTime;
|
||||
$GLOBALS['SIM_ACCESS_TIME'] = $queryTime - $queryTime % 60;
|
||||
$this->context->setAspect('date', new DateTimeAspect(DateTimeFactory::createFromTimestamp($queryTime)));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate user group for preview functionality. When previewing a page with a user group restriction,
|
||||
* the parameter ADMCMD_simUser = <groupId> will be added to the preview url. Simulation happens.
|
||||
* This functionality needs to be loaded after BackendAuthenticator as it is only relevant for
|
||||
* logged in backend users and needs to be done before any page resolving starts.
|
||||
*/
|
||||
protected function simulateUserGroup(ServerRequestInterface $request): bool
|
||||
{
|
||||
$simulateUserGroup = (int)($request->getQueryParams()['ADMCMD_simUser'] ?? 0);
|
||||
if (!$simulateUserGroup) {
|
||||
return false;
|
||||
}
|
||||
$frontendUser = $request->getAttribute('frontend.user');
|
||||
$frontendUser->user[$frontendUser->usergroup_column] = (string)$simulateUserGroup;
|
||||
$frontendUser->userGroups[$simulateUserGroup] = [
|
||||
'uid' => $simulateUserGroup,
|
||||
'title' => '_PREVIEW_',
|
||||
];
|
||||
// let's fake having a user with that group, too
|
||||
$frontendUser->user[$frontendUser->userid_column] = PHP_INT_MAX;
|
||||
// Set this option so the is_online timestamp is not updated in updateOnlineTimestamp()
|
||||
$frontendUser->user['is_online'] = $this->context->getPropertyFromAspect('date', 'timestamp');
|
||||
$this->context->setAspect('frontend.user', $frontendUser->createUserAspect());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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\Frontend\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\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Http\ImmediateResponseException;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\LinkHandling\PageTypeLinkResolver;
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* Redirects pages of type mount points, shortcuts and link to their destination.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class ShortcutAndMountPointRedirect implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected PageTypeLinkResolver $pageTypeLinkResolver,
|
||||
protected LoggerInterface $logger,
|
||||
protected SiteFinder $siteFinder,
|
||||
protected ErrorController $errorController,
|
||||
) {}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$exposeInformation = $GLOBALS['TYPO3_CONF_VARS']['FE']['exposeRedirectInformation'] ?? false;
|
||||
|
||||
// Check for shortcut page and mount point redirect
|
||||
try {
|
||||
$redirectToUri = $this->getRedirectUri($request);
|
||||
} catch (ImmediateResponseException $e) {
|
||||
return $e->getResponse();
|
||||
}
|
||||
/** @var PageArguments $pageArguments */
|
||||
$pageArguments = $request->getAttribute('routing', null);
|
||||
if ($redirectToUri !== null && $redirectToUri !== (string)$request->getUri()) {
|
||||
$message = 'TYPO3 Shortcut/Mountpoint' . ($exposeInformation ? ' at page with ID ' . $pageArguments->getPageId() : '');
|
||||
return new RedirectResponse(
|
||||
$redirectToUri,
|
||||
307,
|
||||
['X-Redirect-By' => $message]
|
||||
);
|
||||
}
|
||||
|
||||
// See if the current page is of doktype "Link", if so, do a redirect as well.
|
||||
$pageInformation = $request->getAttribute('frontend.page.information');
|
||||
$pageRecord = $pageInformation->getPageRecord();
|
||||
if ((int)$pageRecord['doktype'] === PageRepository::DOKTYPE_LINK) {
|
||||
$url = $this->pageTypeLinkResolver->resolvePageLinkUrl($pageRecord, $request);
|
||||
$message = 'TYPO3 Link' . ($exposeInformation ? ' at page with ID ' . $pageArguments->getPageId() : '');
|
||||
$status = $this->pageTypeLinkResolver->getRedirectStatus($pageRecord);
|
||||
|
||||
if ($status !== null && $url !== '') {
|
||||
return new RedirectResponse(
|
||||
$url,
|
||||
$status,
|
||||
['X-Redirect-By' => $message]
|
||||
);
|
||||
}
|
||||
|
||||
$this->logger->error(
|
||||
'Page of type "Link" could not be resolved properly',
|
||||
[
|
||||
'page' => $pageRecord,
|
||||
]
|
||||
);
|
||||
return $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'Page of type "Link" could not be resolved properly',
|
||||
['code' => PageAccessFailureReasons::INVALID_LINK_PAGE]
|
||||
);
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
protected function getRedirectUri(ServerRequestInterface $request): ?string
|
||||
{
|
||||
$redirectToUri = $this->getRedirectUriForShortcut($request);
|
||||
if ($redirectToUri !== null) {
|
||||
return $redirectToUri;
|
||||
}
|
||||
return $this->getRedirectUriForMountPoint($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URI of target page, if the current page is a Shortcut.
|
||||
*
|
||||
* If the current page is of type shortcut and accessed directly via its URL,
|
||||
* the user will be redirected to shortcut target.
|
||||
*/
|
||||
protected function getRedirectUriForShortcut(ServerRequestInterface $request): ?string
|
||||
{
|
||||
$pageInformation = $request->getAttribute('frontend.page.information');
|
||||
$originalShortcutPageRecord = $pageInformation->getOriginalShortcutPageRecord();
|
||||
if (!empty($originalShortcutPageRecord)
|
||||
&& $originalShortcutPageRecord['doktype'] == PageRepository::DOKTYPE_SHORTCUT
|
||||
) {
|
||||
// Check if the shortcut page is actually on the current site, if not, this is a "page not found"
|
||||
// because the request was www.mydomain.com/?id=23 where page ID 23 (which is a shortcut) is on another domain/site.
|
||||
if ((int)($request->getQueryParams()['id'] ?? 0) > 0) {
|
||||
try {
|
||||
$targetSite = $this->siteFinder->getSiteByPageId($originalShortcutPageRecord['l10n_parent'] ?: $originalShortcutPageRecord['uid']);
|
||||
} catch (SiteNotFoundException) {
|
||||
$targetSite = null;
|
||||
}
|
||||
$site = $request->getAttribute('site');
|
||||
if ($targetSite !== $site) {
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'ID was outside the domain',
|
||||
['code' => PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH]
|
||||
);
|
||||
throw new ImmediateResponseException($response, 1638022483);
|
||||
}
|
||||
}
|
||||
return $this->getUriToCurrentPageForRedirect($request);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URI of target page, if the current page is an overlaid mountpoint.
|
||||
*
|
||||
* If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
|
||||
* and is accessed directly, the user will be redirected to the mountpoint context.
|
||||
*/
|
||||
protected function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string
|
||||
{
|
||||
$pageInformation = $request->getAttribute('frontend.page.information');
|
||||
$originalMountPointPageRecord = $pageInformation->getOriginalMountPointPageRecord();
|
||||
if (!empty($originalMountPointPageRecord)
|
||||
&& (int)$originalMountPointPageRecord['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT
|
||||
) {
|
||||
return $this->getUriToCurrentPageForRedirect($request);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string
|
||||
{
|
||||
$pageInformation = $request->getAttribute('frontend.page.information');
|
||||
$pageRecord = $pageInformation->getPageRecord();
|
||||
$parameter = $pageRecord['uid'];
|
||||
/** @var PageArguments $pageArguments */
|
||||
$pageArguments = $request->getAttribute('routing');
|
||||
$type = $pageArguments->getPageType();
|
||||
if ($type) {
|
||||
$parameter .= ',' . $type;
|
||||
}
|
||||
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
||||
$contentObjectRenderer->setRequest($request);
|
||||
return $contentObjectRenderer->createUrl([
|
||||
'parameter' => $parameter,
|
||||
'addQueryString' => 'untrusted',
|
||||
'addQueryString.' => ['exclude' => 'id,type'],
|
||||
'forceAbsoluteUrl' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?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\Frontend\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\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Routing\SiteRouteResult;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* Resolves redirects of site if base is not /
|
||||
* Can be replaced or extended by extensions if GeoIP-based or user-agent based language redirects need to happen.
|
||||
*
|
||||
* Please note that the redirect usually does not contain the Query Parameters, as special query parameters
|
||||
* like "id", "L" and "cHash" could then result in an error loop.
|
||||
* One special case (adding a "/") is keeping the query parameters though.
|
||||
*/
|
||||
class SiteBaseRedirectResolver implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Redirect to default language if required
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$site = $request->getAttribute('site', null);
|
||||
$language = $request->getAttribute('language', null);
|
||||
$routeResult = $request->getAttribute('routing', null);
|
||||
|
||||
// Usually called when "https://www.example.com" was entered, but all sites have "https://www.example.com/lang-key/"
|
||||
// So a redirect to the first possible language is done.
|
||||
if ($site instanceof Site && !($language instanceof SiteLanguage)) {
|
||||
if ($routeResult instanceof SiteRouteResult && $routeResult->getTail() === '') {
|
||||
$language = $site->getDefaultLanguage();
|
||||
if ($language->isEnabled()) {
|
||||
return new RedirectResponse($language->getBase(), 307);
|
||||
}
|
||||
// Default language is disabled, check for the first (enabled) language in list to redirect to that
|
||||
foreach ($site->getLanguages() as $language) {
|
||||
return new RedirectResponse($language->getBase(), 307);
|
||||
}
|
||||
}
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
}
|
||||
|
||||
// Language is found, and hidden but also not visible to the BE user, this needs to fail
|
||||
if ($language instanceof SiteLanguage && !$this->isLanguageEnabled($language, $GLOBALS['BE_USER'] ?? null)) {
|
||||
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$request,
|
||||
'Page is not available in the requested language.',
|
||||
['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
|
||||
);
|
||||
}
|
||||
|
||||
if ($language instanceof SiteLanguage && $routeResult instanceof SiteRouteResult) {
|
||||
$requestedUri = $request->getUri();
|
||||
$tail = $routeResult->getTail();
|
||||
// a URL was called via "/fr-FR/" but the page is actually called "/fr-FR", let's do a redirect
|
||||
if ($tail === '/') {
|
||||
$uri = $requestedUri->withPath(rtrim($requestedUri->getPath(), '/'));
|
||||
return new RedirectResponse($uri, 307);
|
||||
}
|
||||
}
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the language is allowed in Frontend, if not, check if there is valid BE user
|
||||
*/
|
||||
protected function isLanguageEnabled(SiteLanguage $language, ?BackendUserAuthentication $user = null): bool
|
||||
{
|
||||
// language is hidden, check if a possible backend user is allowed to access the language
|
||||
if ($language->enabled() || $user?->checkLanguageAccess($language)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?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\Frontend\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\Core\Localization\Locales;
|
||||
use TYPO3\CMS\Core\Routing\SiteMatcher;
|
||||
use TYPO3\CMS\Core\Routing\SiteRouteResult;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* Identifies if a site is configured for the request, based on "id" and "L" GET/POST parameters, or the requested
|
||||
* string.
|
||||
*
|
||||
* If a site is found, the request is populated with the found language+site objects. If none is found, the main magic
|
||||
* is handled by the PageResolver middleware.
|
||||
*/
|
||||
readonly class SiteResolver implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected SiteMatcher $matcher,
|
||||
protected LoggerInterface $logger,
|
||||
protected ErrorController $errorController,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Resolve the site/language information by checking the page ID or the URL.
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
/** @var SiteRouteResult $routeResult */
|
||||
$routeResult = $this->matcher->matchRequest($request);
|
||||
|
||||
$site = $routeResult->getSite();
|
||||
if ($site instanceof Site && $site->invalidSets !== []) {
|
||||
$invalidSets = implode(', ', array_keys($site->invalidSets));
|
||||
$this->logger->error('Site {identifier} depends on unavailable sets: {invalidSets}', [
|
||||
'identifier' => $site->getIdentifier(),
|
||||
'invalidSets' => $invalidSets,
|
||||
]);
|
||||
return $this->errorController->internalErrorAction(
|
||||
$request,
|
||||
sprintf(
|
||||
'Site %s depends on unavailable sets: %s',
|
||||
$site->getIdentifier(),
|
||||
$invalidSets,
|
||||
),
|
||||
['code' => PageAccessFailureReasons::INVALID_SITE_SETS]
|
||||
);
|
||||
}
|
||||
|
||||
$request = $request->withAttribute('site', $site);
|
||||
$request = $request->withAttribute('language', $routeResult->getLanguage());
|
||||
$request = $request->withAttribute('routing', $routeResult);
|
||||
if ($routeResult->getLanguage() instanceof SiteLanguage) {
|
||||
Locales::setSystemLocaleFromSiteLanguage($routeResult->getLanguage());
|
||||
}
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?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\Frontend\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\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
|
||||
use TYPO3\CMS\Core\Routing\RouterInterface;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceException;
|
||||
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
|
||||
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
|
||||
use TYPO3\CMS\Core\SystemResource\Type\SystemResourceInterface;
|
||||
|
||||
/**
|
||||
* Resolves static routes - can return configured content directly or load content from file / urls
|
||||
*/
|
||||
readonly class StaticRouteResolver implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected RequestFactory $requestFactory,
|
||||
protected LinkService $linkService,
|
||||
protected SystemResourceFactory $systemResourceFactory,
|
||||
protected SystemResourcePublisherInterface $resourcePublisher,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks if there is a valid site with route configuration.
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
if (($site = $request->getAttribute('site')) instanceof Site
|
||||
&& ($configuration = $site->getConfiguration()['routes'] ?? null)
|
||||
) {
|
||||
$path = ltrim($request->getUri()->getPath(), '/');
|
||||
$routeConfig = $this->getApplicableStaticRoute($configuration, $site, $path);
|
||||
if (is_array($routeConfig)) {
|
||||
try {
|
||||
[$content, $contentType] = $this->resolveByType($request, $site, $routeConfig['type'], $routeConfig);
|
||||
} catch (InvalidRouteArgumentsException $e) {
|
||||
return new Response('Invalid route', 404, ['Content-Type' => 'text/plain']);
|
||||
}
|
||||
|
||||
return new HtmlResponse($content, 200, ['Content-Type' => $contentType]);
|
||||
}
|
||||
}
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the proper configuration for the static route in the static route configuration. Mainly:
|
||||
* - needs to have a valid "route" property
|
||||
* - needs to have a "type"
|
||||
*
|
||||
* @param array $staticRouteConfiguration the "routes" part of the site configuration
|
||||
* @param Site $site the current site where the configuration is based on
|
||||
* @param string $uriPath the path of the current request - used to match the "route" value of a single static route
|
||||
* @return array|null the configuration for the static route that matches, or null if no route is given
|
||||
*/
|
||||
protected function getApplicableStaticRoute(array $staticRouteConfiguration, Site $site, string $uriPath): ?array
|
||||
{
|
||||
$routeNames = array_map(static function (?string $route) use ($site) {
|
||||
if ($route === null || $route === '') {
|
||||
return null;
|
||||
}
|
||||
return ltrim(trim($site->getBase()->getPath(), '/') . '/' . ltrim($route, '/'), '/');
|
||||
}, array_column($staticRouteConfiguration, 'route'));
|
||||
// Remove empty routes which would throw an error (could happen within creating a false route in the GUI)
|
||||
$routeNames = array_filter($routeNames);
|
||||
|
||||
if (in_array($uriPath, $routeNames, true)) {
|
||||
$key = array_search($uriPath, $routeNames, true);
|
||||
// Only allow routes with a type "given"
|
||||
if (isset($staticRouteConfiguration[$key]['type'])) {
|
||||
return $staticRouteConfiguration[$key];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getFromFile(File $file): array
|
||||
{
|
||||
$content = $file->getContents();
|
||||
$contentType = $file->getMimeType();
|
||||
return [$content, $contentType];
|
||||
}
|
||||
|
||||
protected function getFromUri(string $uri): array
|
||||
{
|
||||
$response = $this->requestFactory->request($uri);
|
||||
$contentType = 'text/plain; charset=utf-8';
|
||||
$content = '';
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$content = $response->getBody()->getContents();
|
||||
$contentType = $response->getHeader('Content-Type');
|
||||
}
|
||||
|
||||
return [$content, $contentType];
|
||||
}
|
||||
|
||||
protected function getPageUri(ServerRequestInterface $request, Site $site, array $urlParams): string
|
||||
{
|
||||
$parameters = [];
|
||||
// Add additional parameters, if set via TypoLink
|
||||
if (isset($urlParams['parameters'])) {
|
||||
parse_str($urlParams['parameters'], $parameters);
|
||||
}
|
||||
$parameters['type'] = $urlParams['pagetype'] ?? 0;
|
||||
$parameters['_language'] = $request->getAttribute('language', null);
|
||||
$uri = $site->getRouter()->generateUri(
|
||||
(int)($urlParams['pageuid'] ?? 0),
|
||||
$parameters,
|
||||
'',
|
||||
RouterInterface::ABSOLUTE_URL
|
||||
);
|
||||
return (string)$uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidRouteArgumentsException
|
||||
*/
|
||||
protected function resolveByType(ServerRequestInterface $request, Site $site, string $type, array $routeConfig): array
|
||||
{
|
||||
switch ($type) {
|
||||
case 'staticText':
|
||||
if (!isset($routeConfig['content']) || !is_string($routeConfig['content'])) {
|
||||
throw new \InvalidArgumentException('A static route of type "staticText" must have a content defined.', 1704704705);
|
||||
}
|
||||
$content = $routeConfig['content'];
|
||||
$contentType = 'text/plain; charset=utf-8';
|
||||
break;
|
||||
case 'uri':
|
||||
$urlParams = $this->linkService->resolve($routeConfig['source']);
|
||||
if ($urlParams['type'] === 'url' || $urlParams['type'] === 'page') {
|
||||
$uri = $urlParams['url'] ?? $this->getPageUri($request, $site, $urlParams);
|
||||
[$content, $contentType] = $this->getFromUri($uri);
|
||||
} elseif ($urlParams['type'] === 'file') {
|
||||
[$content, $contentType] = $this->getFromFile($urlParams['file']);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Can only handle URIs of type page, url or file.', 1537348076);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'asset':
|
||||
if (!($routeConfig['asset'] ?? null)) {
|
||||
throw new \InvalidArgumentException('A static route of type "asset" must have an asset defined.', 1721134959);
|
||||
}
|
||||
|
||||
try {
|
||||
$resource = $this->systemResourceFactory->createResource($routeConfig['asset']);
|
||||
if (!$resource instanceof SystemResourceInterface) {
|
||||
throw new \InvalidArgumentException(sprintf('The asset "%s" (resolved to "%s") is not a system resource.', $routeConfig['asset'], $resource), 1758785685);
|
||||
}
|
||||
} catch (SystemResourceException $e) {
|
||||
throw new \InvalidArgumentException(sprintf('Could not resolve asset "%s"', $routeConfig['asset']), 1721134960, $e);
|
||||
}
|
||||
|
||||
$content = $resource->getContents();
|
||||
$contentType = $resource->getMimeType();
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException(
|
||||
'Can only handle static file configurations with type uri, staticText or asset',
|
||||
1537348083
|
||||
);
|
||||
}
|
||||
return [$content, $contentType];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?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\Frontend\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\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
|
||||
use TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent;
|
||||
|
||||
/**
|
||||
* Initializes the time tracker (singleton) for the whole TYPO3 Frontend
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class TimeTrackerInitialization implements MiddlewareInterface
|
||||
{
|
||||
private bool $isDebugEnabledInTypoScriptConfig = false;
|
||||
|
||||
public function __construct(private readonly TimeTracker $timeTracker) {}
|
||||
|
||||
/**
|
||||
* Starting time tracking (by setting up a singleton object)
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$timeTrackingEnabled = $this->isBackendUserCookieSet($request);
|
||||
$this->timeTracker->setEnabled($timeTrackingEnabled);
|
||||
$this->timeTracker->start(microtime(true));
|
||||
$this->timeTracker->push('');
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
// Finish time tracking
|
||||
$this->timeTracker->pull();
|
||||
$this->timeTracker->finish();
|
||||
|
||||
if ($this->isDebugModeEnabled()) {
|
||||
return $response->withHeader('X-TYPO3-Parsetime', $this->timeTracker->getParseTime() . 'ms');
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This middleware is run pretty early in the FE chain to initialize correctly.
|
||||
* It however should only add the response header if debugging is enabled in TypoScript 'config',
|
||||
* which is not available in the incoming Request, yet.
|
||||
* It thus listens on the AfterTypoScriptDeterminedEvent to set $this->isDebugEnabledInTypoScriptConfig.
|
||||
*/
|
||||
#[AsEventListener('typo3-frontend/timetracker-init-middleware')]
|
||||
public function typoScriptDeterminedListener(AfterTypoScriptDeterminedEvent $event): void
|
||||
{
|
||||
$typoScriptConfig = $event->getFrontendTypoScript()->getConfigArray();
|
||||
if (!empty($typoScriptConfig['debug'] ?? false)) {
|
||||
$this->isDebugEnabledInTypoScriptConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
private function isBackendUserCookieSet(ServerRequestInterface $request): bool
|
||||
{
|
||||
$configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']) ?: 'be_typo_user';
|
||||
return !empty($request->getCookieParams()[$configuredCookieName]);
|
||||
}
|
||||
|
||||
private function isDebugModeEnabled(): bool
|
||||
{
|
||||
if ($this->isDebugEnabledInTypoScriptConfig) {
|
||||
return true;
|
||||
}
|
||||
return !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user