first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,153 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* 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.
*
* Currently, this middleware depends on the availability of $GLOBALS['TSFE'], however, this is solely
* due to backwards-compatibility and will be disabled in the future.
*/
class BackendUserAuthenticator implements MiddlewareInterface
{
/**
* Creates a frontend user authentication object, tries to authenticate a user
* and stores the object in $GLOBALS['TSFE']->fe_user.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// PRE BE_USER HOOK
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preBeUser'])) {
trigger_error('The "preBeUser" hook will be removed in TYPO3 v10.0 in favor of PSR-15. Use a middleware instead.', E_USER_DEPRECATED);
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preBeUser'] as $_funcRef) {
$_params = [];
GeneralUtility::callUserFunction($_funcRef, $_params, $GLOBALS['TSFE']);
}
}
// 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;
// POST BE_USER HOOK
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['postBeUser'])) {
$_params = [
'BE_USER' => &$GLOBALS['BE_USER']
];
trigger_error('The "postBeUser" hook will be removed in TYPO3 v10.0 in favor of PSR-15. Use a middleware instead.', E_USER_DEPRECATED);
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['postBeUser'] as $_funcRef) {
GeneralUtility::callUserFunction($_funcRef, $_params, $GLOBALS['TSFE']);
}
}
// Load specific dependencies which are necessary for a valid Backend User
// like $GLOBALS['LANG'] for labels in the language of the BE User, the router, and ext_tables.php for all modules
// So things like Frontend Editing and Admin Panel can use this for generating links to the TYPO3 Backend.
if ($GLOBALS['BE_USER'] instanceof FrontendBackendUserAuthentication) {
Bootstrap::initializeLanguageObject();
Bootstrap::initializeBackendRouter();
Bootstrap::loadExtTables();
$this->setBackendUserAspect(GeneralUtility::makeInstance(Context::class), $GLOBALS['BE_USER']);
}
return $handler->handle($request);
}
/**
* Creates the backend user object and returns it.
*
* @param ServerRequestInterface $request
* @return FrontendBackendUserAuthentication|null the backend user object or null if there was no valid user found
* @throws \TYPO3\CMS\Core\Exception
*/
protected function initializeBackendUser(ServerRequestInterface $request)
{
// New backend user object
$backendUserObject = GeneralUtility::makeInstance(FrontendBackendUserAuthentication::class);
$backendUserObject->start();
$backendUserObject->unpack_uc();
if (!empty($backendUserObject->user['uid'])) {
$backendUserObject->fetchGroupData();
}
// Unset the user initialization if any setting / restriction applies
if (!$this->isAuthenticated($backendUserObject, $request->getAttribute('normalizedParams'))) {
$backendUserObject = null;
}
return $backendUserObject;
}
/**
* Implementing the access checks that the TYPO3 CMS bootstrap script does before a user is ever logged in.
*
* @param FrontendBackendUserAuthentication $user
* @param NormalizedParams $normalizedParams
* @return bool Returns TRUE if access is OK
*/
protected function isAuthenticated(FrontendBackendUserAuthentication $user, NormalizedParams $normalizedParams)
{
// Check IP
$ipMask = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList'] ?? '');
if ($ipMask && !GeneralUtility::cmpIP($normalizedParams->getRemoteAddress(), $ipMask)) {
return false;
}
// Check SSL (https)
if ((bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] && !$normalizedParams->isHttps()) {
return false;
}
return $user->backendCheckLogin();
}
/**
* Register the backend user as aspect
*
* @param Context $context
* @param BackendUserAuthentication|null $user
*/
protected function setBackendUserAspect(Context $context, BackendUserAuthentication $user)
{
$context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user));
$context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user->workspace));
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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\NullResponse;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Send content-length header.
* Notice that all HTML content outside the length of the content-length header will be cut off!
* Therefore content of unknown length from included PHP-scripts and if admin users are logged
* in (admin panel might show...) or if debug mode is turned on, we disable it!
*
* @internal
*/
class ContentLengthResponseHeader implements MiddlewareInterface
{
/**
* Adds the content length
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
if (
!($response instanceof NullResponse)
&& $GLOBALS['TSFE'] instanceof TypoScriptFrontendController
&& $GLOBALS['TSFE']->isOutputting()) {
if (
(!isset($GLOBALS['TSFE']->config['config']['enableContentLengthHeader']) || $GLOBALS['TSFE']->config['config']['enableContentLengthHeader'])
&& !$GLOBALS['TSFE']->isBackendUserLoggedIn() && !$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
&& !$GLOBALS['TSFE']->config['config']['debug'] && !$GLOBALS['TSFE']->doWorkspacePreview()
) {
$response = $response->withHeader('Content-Length', (string)$response->getBody()->getSize());
}
}
return $response;
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Exception;
use TYPO3\CMS\Core\Http\Dispatcher;
use TYPO3\CMS\Core\Http\NullResponse;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* 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
*/
class EidHandler implements MiddlewareInterface
{
/**
* Dispatches the request to the corresponding eID class or eID script
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
* @throws Exception
*/
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();
/** @var Response $response */
$response = GeneralUtility::makeInstance(Response::class);
if (empty($eID) || !isset($GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include'][$eID])) {
return $response->withStatus(404, 'eID not registered');
}
$configuration = $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include'][$eID];
// Simple check to make sure that it's not an absolute file (to use the fallback)
if (strpos($configuration, '::') !== false || is_callable($configuration)) {
/** @var Dispatcher $dispatcher */
$dispatcher = GeneralUtility::makeInstance(Dispatcher::class);
$request = $request->withAttribute('target', $configuration);
return $dispatcher->dispatch($request, $response) ?? new NullResponse();
}
trigger_error(
'eID "' . $eID . '" is registered with a script to a file. This behaviour will be removed in TYPO3 v10.0.'
. ' Register eID with a class::method syntax like "\MyVendor\MyExtension\Controller\MyEidController::myMethod" instead.',
E_USER_DEPRECATED
);
$scriptPath = GeneralUtility::getFileAbsFileName($configuration);
if ($scriptPath === '') {
throw new Exception('Registered eID has invalid script path.', 1518042216);
}
include $scriptPath;
return new NullResponse();
}
}
@@ -0,0 +1,131 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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\AbstractUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
/**
* This middleware authenticates a Frontend User (fe_users).
* A valid $GLOBALS['TSFE'] object is needed for the time being, being fully backwards-compatible.
*/
class FrontendUserAuthenticator implements MiddlewareInterface
{
/**
* Creates a frontend user authentication object, tries to authenticate a user
* and stores the object in $GLOBALS['TSFE']->fe_user.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class);
// List of page IDs where to look for frontend user records
$pid = $request->getParsedBody()['pid'] ?? $request->getQueryParams()['pid'] ?? 0;
if ($pid) {
$frontendUser->checkPid_value = implode(',', GeneralUtility::intExplode(',', $pid));
}
// Check if a session is transferred, and update the cookie parameters
$frontendSessionKey = $request->getParsedBody()['FE_SESSION_KEY'] ?? $request->getQueryParams()['FE_SESSION_KEY'] ?? '';
if ($frontendSessionKey) {
$request = $this->transferFrontendUserSession($frontendUser, $request, $frontendSessionKey);
}
// Authenticate now
$frontendUser->start();
$frontendUser->unpack_uc();
// Keep the backwards-compatibility for TYPO3 v9, to have the fe_user within the global TSFE object
$GLOBALS['TSFE']->fe_user = $frontendUser;
// Call hook for possible manipulation of frontend user object
// This hook is kept for compatibility reasons, however, it should be fairly simple to add a custom middleware
// for this purpose
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['initFEuser'])) {
trigger_error('The "initFEuser" hook will be removed in TYPO3 v10.0 in favor of PSR-15. Use a middleware instead.', E_USER_DEPRECATED);
$_params = ['pObj' => &$GLOBALS['TSFE']];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['initFEuser'] as $_funcRef) {
GeneralUtility::callUserFunction($_funcRef, $_params, $GLOBALS['TSFE']);
}
}
// Register the frontend user as aspect
$this->setFrontendUserAspect(GeneralUtility::makeInstance(Context::class), $frontendUser);
return $handler->handle($request);
}
/**
* It's possible to transfer a frontend user session via a GET/POST parameter 'FE_SESSION_KEY'.
* In the future, this logic should be moved into the FrontendUserAuthentication object directly,
* but only if FrontendUserAuthentication does not request superglobals (like $_COOKIE) anymore.
*
* @param FrontendUserAuthentication $frontendUser
* @param ServerRequestInterface $request
* @param string $frontendSessionKey
* @return ServerRequestInterface
*/
protected function transferFrontendUserSession(
FrontendUserAuthentication $frontendUser,
ServerRequestInterface $request,
string $frontendSessionKey
): ServerRequestInterface {
list($sessionId, $hash) = explode('-', $frontendSessionKey);
// If the session key hash check is OK, set the cookie
if (hash_equals(md5($sessionId . '/' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']), (string)$hash)) {
$cookieName = FrontendUserAuthentication::getCookieName();
// keep the global cookie overwriting for now, as long as FrontendUserAuthentication does not
// use the request object for fetching the cookie information.
$_COOKIE[$cookieName] = $sessionId;
if (isset($_SERVER['HTTP_COOKIE'])) {
// See http://forge.typo3.org/issues/27740
$_SERVER['HTTP_COOKIE'] .= ';' . $cookieName . '=' . $sessionId;
}
// Add the cookie to the Server Request object
$cookieParams = $request->getCookieParams();
$cookieParams[$cookieName] = $sessionId;
$request = $request->withCookieParams($cookieParams);
// @deprecated: we override the current request because it was enriched by cookie information here.
$GLOBALS['TYPO3_REQUEST'] = $request;
$frontendUser->forceSetCookie = true;
$frontendUser->dontSetCookie = false;
}
return $request;
}
/**
* Register the frontend user as aspect
*
* @param Context $context
* @param AbstractUserAuthentication $user
*/
protected function setFrontendUserAspect(Context $context, AbstractUserAuthentication $user)
{
$context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $user));
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
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,61 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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\Core\Utility\MathUtility;
use TYPO3\CMS\Frontend\Utility\CompressionUtility;
/**
* Sets up output compression
*
* @internal
*/
class OutputCompression implements MiddlewareInterface
{
/**
* Clears all output and checks if a compression level is set
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Throw away all output that may have happened during bootstrapping by weird extensions
ob_clean();
// Initialize output compression if configured
$this->initializeOutputCompression();
return $handler->handle($request);
}
/**
* Initialize output compression if configured
*/
protected function initializeOutputCompression()
{
if ($GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] && extension_loaded('zlib')) {
if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'])) {
@ini_set('zlib.output_compression_level', (string)$GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel']);
}
ob_start([GeneralUtility::makeInstance(CompressionUtility::class), 'compressionOutputHandler']);
}
}
}
@@ -0,0 +1,149 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
/**
* This middleware validates given request parameters against the common "cHash" functionality.
*/
class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* The cHash Service class used for cHash related functionality
*
* @var CacheHashCalculator
*/
protected $cacheHashCalculator;
/**
* @var TypoScriptFrontendController
*/
protected $controller;
/**
* @param TypoScriptFrontendController|null $controller
*/
public function __construct(TypoScriptFrontendController $controller = null)
{
$this->controller = $controller ?? $GLOBALS['TSFE'];
$this->cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
}
/**
* Validates the &cHash parameter against the other $queryParameters / GET parameters
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$pageNotFoundOnValidationError = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] ?? true);
$pageArguments = $request->getAttribute('routing', null);
if ($this->controller->no_cache && !$pageNotFoundOnValidationError) {
// No need to test anything if caching was already disabled.
} else {
// Evaluate the cache hash parameter or dynamic arguments when coming from a Site-based routing
if ($pageArguments instanceof PageArguments) {
$queryParams = $pageArguments->getDynamicArguments();
} else {
$queryParams = $request->getQueryParams();
}
if (!empty($queryParams) && !$this->evaluateCacheHashParameter($queryParams, $pageNotFoundOnValidationError)) {
// cHash was given, but nothing to be calculated, so let's do a redirect to the current page
// but without the cHash
if ($this->controller->cHash && empty($this->controller->cHash_array)) {
$uri = $request->getUri();
unset($queryParams['cHash']);
$uri = $uri->withQuery(HttpUtility::buildQueryString($queryParams));
return new RedirectResponse($uri, 308);
}
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'Request parameters could not be validated (&cHash comparison failed)',
['code' => PageAccessFailureReasons::CACHEHASH_COMPARISON_FAILED]
);
}
}
return $handler->handle($request);
}
/**
* Calculates a hash string based on additional parameters in the url.
*
* Calculated hash is stored in $this->controller->cHash_array.
* This is used to cache pages with more parameters than just id and type.
*
* @see TypoScriptFrontendController::reqCHash()
* @param array<string, string> $queryParams GET parameters
* @param bool $pageNotFoundOnCacheHashError see $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']
* @return bool if false, then a PageNotFound response is triggered
*/
protected function evaluateCacheHashParameter(array $queryParams, bool $pageNotFoundOnCacheHashError): bool
{
if ($this->controller->cHash !== '') {
// Make sure we use the page uid and not the page alias
$queryParams['id'] = $this->controller->id;
$relevantParameters = $this->cacheHashCalculator->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
$this->controller->cHash_array = $relevantParameters;
// cHash was given, but nothing to be calculated, so cHash is unset and all is good.
if (empty($relevantParameters)) {
$this->logger->notice('The incoming cHash "' . $this->controller->cHash . '" is given but not needed. cHash is unset');
return false;
}
$calculatedCacheHash = $this->cacheHashCalculator->calculateCacheHash($relevantParameters);
if (!hash_equals($calculatedCacheHash, $this->controller->cHash)) {
// Early return to trigger the error controller
if ($pageNotFoundOnCacheHashError) {
return false;
}
$this->controller->no_cache = true;
$this->getTimeTracker()->setTSlogMessage('The incoming cHash "' . $this->controller->cHash . '" and calculated cHash "' . $calculatedCacheHash . '" did not match, so caching was disabled. The fieldlist used was "' . implode(',', array_keys($this->controller->cHash_array)) . '"', 2);
}
// No cHash is set, check if that is correct
} elseif ($this->cacheHashCalculator->doParametersRequireCacheHash(HttpUtility::buildQueryString($queryParams))) {
// Will disable caching
$this->controller->reqCHash();
}
return true;
}
/**
* @return TimeTracker
*/
protected function getTimeTracker(): TimeTracker
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
}
@@ -0,0 +1,242 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction;
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\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
/**
* Process the ID, type and other parameters.
* After this point we have an array, TSFE->page, which is the page-record of the current page, $TSFE->id.
*
* Now, if there is a backend user logged in and he has NO access to this page,
* then re-evaluate the id shown!
*/
class PageResolver implements MiddlewareInterface
{
/**
* @var TypoScriptFrontendController
*/
protected $controller;
public function __construct(TypoScriptFrontendController $controller = null)
{
$this->controller = $controller ?? $GLOBALS['TSFE'];
}
/**
* Resolve the page ID
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// First, resolve the root page of the site, the Page ID of the current domain
if (($site = $request->getAttribute('site', null)) instanceof SiteInterface) {
$this->controller->domainStartPage = $site->getRootPageId();
}
$language = $request->getAttribute('language', null);
$hasSiteConfiguration = $language instanceof SiteLanguage && $site instanceof Site;
// Resolve the page ID based on TYPO3's native routing functionality
if ($hasSiteConfiguration) {
/** @var SiteRouteResult $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]
);
}
$requestId = (string)($request->getQueryParams()['id'] ?? '');
if (!empty($requestId)) {
$page = $this->resolvePageId($requestId);
if ($page === null) {
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'The requested page does not exist',
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
);
}
// Legacy URIs (?id=12345) takes precedence, not matter if a route is given
$pageArguments = new PageArguments(
(int)($page['l10n_parent'] ?: $page['uid']),
(string)($request->getQueryParams()['type'] ?? '0'),
[],
[],
$request->getQueryParams()
);
} else {
// Check for the route
try {
$pageArguments = $site->getRouter()->matchRequest($request, $previousResult);
} 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]
);
}
$this->controller->id = $pageArguments->getPageId();
$this->controller->type = $pageArguments->getPageType() ?? $this->controller->type;
$this->controller->MP = $pageArguments->getArguments()['MP'] ?? $this->controller->MP;
$request = $request->withAttribute('routing', $pageArguments);
// 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);
$this->controller->setPageArguments($pageArguments);
// At this point, we later get further route modifiers
// for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE.
$GLOBALS['TYPO3_REQUEST'] = $request;
} else {
// old-school page resolving for realurl, cooluri etc.
$this->controller->siteScript = $request->getAttribute('normalizedParams')->getSiteScript();
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['checkAlternativeIdMethods-PostProc'])) {
trigger_error('The "checkAlternativeIdMethods-PostProc" hook will be removed in TYPO3 v10.0 in favor of PSR-15. Use a middleware instead.', E_USER_DEPRECATED);
$this->checkAlternativeIdMethods($this->controller);
}
}
$this->controller->determineId();
// No access? Then remove user & Re-evaluate the page-id
if ($this->controller->isBackendUserLoggedIn() && !$GLOBALS['BE_USER']->doesUserHaveAccess($this->controller->page, Permission::PAGE_SHOW)) {
unset($GLOBALS['BE_USER']);
// Register an empty backend user as aspect
$this->setBackendUserAspect(GeneralUtility::makeInstance(Context::class), null);
if (!$hasSiteConfiguration) {
$this->checkAlternativeIdMethods($this->controller);
}
$this->controller->determineId();
}
return $handler->handle($request);
}
/**
* Provides ways to bypass the '?id=[xxx]&type=[xx]' format, using either PATH_INFO or Server Rewrites
*
* Two options:
* 1) Use PATH_INFO (also Apache) to extract id and type from that var. Does not require any special modules compiled with apache. (less typical)
* 2) Using hook which enables features like those provided from "realurl" extension (AKA "Speaking URLs")
*
* @param TypoScriptFrontendController $tsfe
*/
protected function checkAlternativeIdMethods(TypoScriptFrontendController $tsfe)
{
// Call post processing function for custom URL methods.
$_params = ['pObj' => &$tsfe];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['checkAlternativeIdMethods-PostProc'] ?? [] as $_funcRef) {
GeneralUtility::callUserFunction($_funcRef, $_params, $tsfe);
}
}
/**
* @param string $pageId
* @return array|null
*/
protected function resolvePageId(string $pageId): ?array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
$queryBuilder
->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class));
if (MathUtility::canBeInterpretedAsInteger($pageId)) {
$constraint = $queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
);
} else {
$constraint = $queryBuilder->expr()->eq(
'alias',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_STR)
);
}
$statement = $queryBuilder
->select('uid', 'l10n_parent', 'pid')
->from('pages')
->where($constraint)
->execute();
$page = $statement->fetch();
if (empty($page)) {
return null;
}
return $page;
}
/**
* Register the backend user as aspect
*
* @param Context $context
* @param BackendUserAuthentication $user
*/
protected function setBackendUserAspect(Context $context, BackendUserAuthentication $user = null)
{
$context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user));
$context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user ? $user->workspace : 0));
}
}
@@ -0,0 +1,137 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as PsrRequestHandlerInterface;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Initialization of TypoScriptFrontendController
*
* Do all necessary preparation steps for rendering
*
* @internal this middleware might get removed in TYPO3 v10.0.
*/
class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
{
/**
* @var TypoScriptFrontendController
*/
protected $controller;
/**
* @var TimeTracker
*/
protected $timeTracker;
public function __construct(TypoScriptFrontendController $controller = null, TimeTracker $timeTracker = null)
{
$this->controller = $controller ?: $GLOBALS['TSFE'];
$this->timeTracker = $timeTracker ?: GeneralUtility::makeInstance(TimeTracker::class);
}
/**
* Initialize TypoScriptFrontendController to the point right before rendering of the page is triggered
*
* @param ServerRequestInterface $request
* @param PsrRequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, PsrRequestHandlerInterface $handler): ResponseInterface
{
// Get from cache
$this->timeTracker->push('Get Page from cache');
// Locks may be acquired here
$this->controller->getFromCache();
$this->timeTracker->pull();
// Get config if not already gotten
// After this, we should have a valid config-array ready
$this->controller->getConfigArray();
// Merge Query Parameters with config.defaultGetVars
// This is done in getConfigArray as well, but does not override the current middleware request object
// Since we want to stay in sync with this, the option needs to be set as well.
$pageArguments = $request->getAttribute('routing');
if (!empty($this->controller->config['config']['defaultGetVars.'] ?? null)) {
$modifiedGetVars = GeneralUtility::removeDotsFromTS($this->controller->config['config']['defaultGetVars.']);
if ($pageArguments instanceof PageArguments) {
$pageArguments = $pageArguments->withQueryArguments($modifiedGetVars);
$this->controller->setPageArguments($pageArguments);
$request = $request->withAttribute('routing', $pageArguments);
}
if (!empty($request->getQueryParams())) {
ArrayUtility::mergeRecursiveWithOverrule($modifiedGetVars, $request->getQueryParams());
}
$request = $request->withQueryParams($modifiedGetVars);
$GLOBALS['TYPO3_REQUEST'] = $request;
}
// Setting language and locale
$this->timeTracker->push('Setting language and locale');
$this->controller->settingLanguage();
$this->controller->settingLocale();
$this->timeTracker->pull();
// Convert POST data to utf-8 for internal processing if metaCharset is different
if ($this->controller->metaCharset !== 'utf-8' && is_array($_POST) && !empty($_POST)) {
$this->convertCharsetRecursivelyToUtf8($_POST, $this->controller->metaCharset);
$GLOBALS['HTTP_POST_VARS'] = $_POST;
$parsedBody = $request->getParsedBody();
$this->convertCharsetRecursivelyToUtf8($parsedBody, $this->controller->metaCharset);
$request = $request->withParsedBody($parsedBody);
$GLOBALS['TYPO3_REQUEST'] = $request;
}
// @deprecated since TYPO3 v9.3, will be removed in TYPO3 v10.0
$this->controller->initializeRedirectUrlHandlers(true);
// Hook for processing data submission to extensions
// This is done at this point, because we need the config values
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['checkDataSubmission'])) {
trigger_error('The "checkDataSubmission" hook will be removed in TYPO3 v10.0 in favor of PSR-15. Use a middleware instead.', E_USER_DEPRECATED);
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['checkDataSubmission'] as $className) {
GeneralUtility::makeInstance($className)->checkDataSubmission($this->controller);
}
}
return $handler->handle($request);
}
/**
* Small helper function to convert charsets for arrays to UTF-8
*
* @param mixed $data given by reference (string/array usually)
* @param string $fromCharset convert FROM this charset
*/
protected function convertCharsetRecursivelyToUtf8(&$data, string $fromCharset)
{
foreach ($data as $key => $value) {
if (is_array($data[$key])) {
$this->convertCharsetRecursivelyToUtf8($data[$key], $fromCharset);
} elseif (is_string($data[$key])) {
$data[$key] = mb_convert_encoding($data[$key], 'utf-8', $fromCharset);
}
}
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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;
/**
* Calls a hook before processing a request for the TYPO3 Frontend.
*
* @internal
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
*/
class PreprocessRequestHook implements MiddlewareInterface
{
/**
* Hook to preprocess the current request
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Legacy functionality to check if any hook modified global GET/POST
// This is a safety net, see RequestHandler for how this is validated.
// This information is just a compat layer which will be removed in TYPO3 v10.0.
$request = $request->withAttribute('_originalGetParameters', $_GET);
if ($request->getMethod() === 'POST') {
$request = $request->withAttribute('_originalPostParameters', $_POST);
}
// Set original parameters
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preprocessRequest'])) {
trigger_error('The "preprocessRequest" hook will be removed in TYPO3 v10.0 in favor of PSR-15. Use a middleware instead.', E_USER_DEPRECATED);
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preprocessRequest'] as $hookFunction) {
$hookParameters = [];
GeneralUtility::callUserFunction($hookFunction, $hookParameters, $hookParameters);
}
}
return $handler->handle($request);
}
}
@@ -0,0 +1,135 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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\UserAspect;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* 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
*/
class PreviewSimulator implements MiddlewareInterface
{
/**
* @var \TYPO3\CMS\Core\Context\Context
*/
private $context;
public function __construct()
{
$this->context = GeneralUtility::makeInstance(Context::class);
}
/**
* Evaluates preview settings if a backend user is logged in
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
* @throws \Exception
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ((bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) {
$simulatingDate = $this->simulateDate($request);
$simulatingGroup = $this->simulateUserGroup($request);
$showHiddenRecords = ($this->context->hasAspect('visibility') ? $this->context->getAspect('visibility')->includeHidden() : false);
$GLOBALS['TSFE']->fePreview = ($simulatingDate || $simulatingGroup || $showHiddenRecords);
}
return $handler->handle($request);
}
/**
* 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.
*
* @param ServerRequestInterface $request
* @return bool
*/
protected function simulateDate(ServerRequestInterface $request): bool
{
$simulatedDate = null;
$queryTime = $request->getQueryParams()['ADMCMD_simTime'] ?? false;
if (!$queryTime) {
return false;
}
$simulatedDate = new \DateTimeImmutable('@' . $queryTime);
if (!$simulatedDate) {
return false;
}
$GLOBALS['SIM_EXEC_TIME'] = $queryTime;
$GLOBALS['SIM_ACCESS_TIME'] = $queryTime - $queryTime % 60;
$this->context->setAspect(
'date',
GeneralUtility::makeInstance(
DateTimeAspect::class,
$simulatedDate
)
);
return true;
}
/**
* Simulate user group for preview functionality
* When previewing a page with a usergroup restriction, the parameter ADMCMD_simUser = <groupId> will be added
* to the preview url. Simulation happens.
* legacy: via TSFE member variables (->fe_user->user[<groupColumn>])
* new: via Context::UserAspect
* 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.
*
* @param ServerRequestInterface $request
* @return bool
*/
protected function simulateUserGroup(ServerRequestInterface $request): bool
{
$simulateUserGroup = (int)($request->getQueryParams()['ADMCMD_simUser'] ?? 0);
if (!$simulateUserGroup) {
return false;
}
$frontendUser = $GLOBALS['TSFE']->fe_user;
$frontendUser->user[$frontendUser->usergroup_column] = $simulateUserGroup;
// let's fake having a user with that group, too
$frontendUser->user['uid'] = PHP_INT_MAX;
$this->context->setAspect(
'frontend.user',
GeneralUtility::makeInstance(
UserAspect::class,
$frontendUser,
[$simulateUserGroup]
)
);
return true;
}
}
@@ -0,0 +1,98 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Page\PageRepository;
/**
* Checks mount points, shortcuts and redirects to the target.
* Alternatively, checks if the current page is an redirect to an external page
*
* @internal this middleware might get removed in TYPO3 v10.0.
*/
class ShortcutAndMountPointRedirect implements MiddlewareInterface
{
/**
* @var TypoScriptFrontendController
*/
private $controller;
public function __construct(TypoScriptFrontendController $controller = null)
{
$this->controller = $controller ?: $GLOBALS['TSFE'];
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Check for shortcut page and mount point redirect
$redirectToUri = $this->getRedirectUri($request);
if ($redirectToUri !== null && $redirectToUri !== (string)$request->getUri()) {
return new RedirectResponse($redirectToUri, 307);
}
// See if the current page is of doktype "External URL", if so, do a redirect as well.
if (empty($this->controller->config['config']['disablePageExternalUrl'] ?? null)
&& PageRepository::DOKTYPE_LINK === (int)$this->controller->page['doktype']) {
$externalUrl = $this->prefixExternalPageUrl(
$this->controller->page['url'],
$request->getAttribute('normalizedParams')->getSiteUrl()
);
if (!empty($externalUrl)) {
return new RedirectResponse($externalUrl, 303);
}
}
return $handler->handle($request);
}
protected function getRedirectUri(ServerRequestInterface $request): ?string
{
$redirectToUri = $this->controller->getRedirectUriForShortcut($request);
if ($redirectToUri !== null) {
return $redirectToUri;
}
return $this->controller->getRedirectUriForMountPoint($request);
}
/**
* Returns the redirect URL for the input page row IF the doktype is set to 3.
*
* @param string $redirectTo The page row to return URL type for
* @param string $sitePrefix if no protocol or relative path given, the site prefix is added
* @return string The URL from based on the external page URL given with a prefix.
*/
protected function prefixExternalPageUrl(string $redirectTo, string $sitePrefix): string
{
$uI = parse_url($redirectTo);
// If relative path, prefix Site URL
// If it's a valid email without protocol, add "mailto:"
if (!($uI['scheme'] ?? false)) {
if (GeneralUtility::validEmail($redirectTo)) {
$redirectTo = 'mailto:' . $redirectTo;
} elseif ($redirectTo[0] !== '/') {
$redirectTo = $sitePrefix . $redirectTo;
}
}
return $redirectTo;
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
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();
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
*
* @param SiteLanguage|null $language
* @param BackendUserAuthentication|null $user
* @return bool
*/
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 instanceof BackendUserAuthentication && $user->checkLanguageAccess($language->getLanguageId()))) {
return true;
}
return false;
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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\SiteMatcher;
use TYPO3\CMS\Core\Routing\SiteRouteResult;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* 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.
*/
class SiteResolver implements MiddlewareInterface
{
/**
* @var SiteMatcher
*/
protected $matcher;
public function __construct(SiteMatcher $matcher = null)
{
$this->matcher = $matcher ?? GeneralUtility::makeInstance(
SiteMatcher::class,
GeneralUtility::makeInstance(SiteFinder::class)
);
}
/**
* Resolve the site/language information by checking the page ID or the URL.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var SiteRouteResult $routeResult */
$routeResult = $this->matcher->matchRequest($request);
$request = $request->withAttribute('site', $routeResult->getSite());
$request = $request->withAttribute('language', $routeResult->getLanguage());
$request = $request->withAttribute('routing', $routeResult);
// At this point, we later get further route modifiers
// for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE.
$GLOBALS['TYPO3_REQUEST'] = $request;
return $handler->handle($request);
}
}
@@ -0,0 +1,186 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
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\Utility\GeneralUtility;
/**
* Resolves static routes - can return configured content directly or load content from file / urls
*/
class StaticRouteResolver implements MiddlewareInterface
{
/**
* Checks if there is a valid site with route configuration.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (($site = $request->getAttribute('site', null)) 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(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;
}
/**
* @param File $file
* @return array
*/
protected function getFromFile(File $file): array
{
$content = $file->getContents();
$contentType = $file->getMimeType();
return [$content, $contentType];
}
/**
* @param string $uri
* @return array
*/
protected function getFromUri(string $uri): array
{
$requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
$response = $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];
}
/**
* @param ServerRequestInterface $request
* @param Site $site
* @param array $urlParams
* @return string
* @throws InvalidRouteArgumentsException
*/
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'],
$parameters,
'',
RouterInterface::ABSOLUTE_URL
);
return (string)$uri;
}
/**
* @param ServerRequestInterface $request
* @param Site $site
* @param string $type
* @param array $routeConfig
* @return array
* @throws InvalidRouteArgumentsException
*/
protected function resolveByType(ServerRequestInterface $request, Site $site, string $type, array $routeConfig): array
{
switch ($type) {
case 'staticText':
$content = $routeConfig['content'];
$contentType = 'text/plain; charset=utf-8';
break;
case 'uri':
$linkService = GeneralUtility::makeInstance(LinkService::class);
$urlParams = $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;
default:
throw new \InvalidArgumentException(
'Can only handle static file configurations with type uri or staticText.',
1537348083
);
}
return [$content, $contentType];
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Initializes the time tracker (singleton) for the whole TYPO3 Frontend
*
* @internal
*/
class TimeTrackerInitialization implements MiddlewareInterface
{
/**
* Starting time tracking (by setting up a singleton object)
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']) ?: 'be_typo_user';
$timeTracker = GeneralUtility::makeInstance(
TimeTracker::class,
!empty($request->getCookieParams()[$configuredCookieName])
);
$timeTracker->start();
$timeTracker->push('');
return $handler->handle($request);
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Frontend\Middleware;
/*
* 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!
*/
use Doctrine\DBAL\Exception\ConnectionException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Creates an instance of TypoScriptFrontendController and makes this globally available
* via $GLOBALS['TSFE'].
*
* For now, GeneralUtility::_GP() is used in favor of $request->getQueryParams() due to
* hooks who could have $_GET/$_POST modified before.
*
* @internal this middleware might get removed in TYPO3 v10.0.
*/
class TypoScriptFrontendInitialization implements MiddlewareInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* Creates an instance of TSFE and sets it as a global variable,
* also pings the database in order ensure a valid database connection.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$GLOBALS['TSFE'] = GeneralUtility::makeInstance(
TypoScriptFrontendController::class,
null,
GeneralUtility::_GP('id'),
GeneralUtility::_GP('type'),
null,
GeneralUtility::_GP('cHash'),
null,
GeneralUtility::_GP('MP')
);
if (GeneralUtility::_GP('no_cache')) {
$GLOBALS['TSFE']->set_no_cache('&no_cache=1 has been supplied, so caching is disabled! URL: "' . (string)$request->getUri() . '"');
}
// Set up the database connection and see if the connection can be established
try {
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
$connection->connect();
} catch (ConnectionException | \RuntimeException $exception) {
$message = 'Cannot connect to the configured database';
$this->logger->emergency($message, ['exception' => $exception]);
try {
return GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($request, $message);
} catch (ServiceUnavailableException $e) {
throw new ServiceUnavailableException($message, 1526013723);
}
}
// Call post processing function for DB connection:
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['connectToDB'])) {
trigger_error('The "connectToDB" hook will be removed in TYPO3 v10.0 in favor of PSR-15. Use a middleware instead.', E_USER_DEPRECATED);
$_params = ['pObj' => &$GLOBALS['TSFE']];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['connectToDB'] as $_funcRef) {
GeneralUtility::callUserFunction($_funcRef, $_params, $GLOBALS['TSFE']);
}
}
return $handler->handle($request);
}
}