diff --git a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php index 352802aec7d65fe52271f504ae26f7c277c2aff8..0213398f0c6ba07daaa00b147af3883b1739f678 100644 --- a/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php @@ -17,12 +17,8 @@ namespace TYPO3\CMS\Backend\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\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\Localization\LanguageService; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -32,7 +28,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * * @internal */ -class BackendUserAuthenticator implements MiddlewareInterface +class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator { /** * List of requests that don't need a valid BE user @@ -50,16 +46,6 @@ class BackendUserAuthenticator implements MiddlewareInterface '/ajax/core/requirejs', ]; - /** - * @var Context - */ - protected $context; - - public function __construct(Context $context) - { - $this->context = $context; - } - /** * Calls the bootstrap process to set up $GLOBALS['BE_USER'] AND $GLOBALS['LANG'] * @@ -71,14 +57,21 @@ class BackendUserAuthenticator implements MiddlewareInterface { $pathToRoute = $request->getAttribute('routePath', '/login'); - Bootstrap::initializeBackendUser(); + // The global must be available very early, because methods below + // might trigger code which relies on it. See: #45625 + $GLOBALS['BE_USER'] = GeneralUtility::makeInstance(BackendUserAuthentication::class); + $GLOBALS['BE_USER']->start(); // @todo: once this logic is in this method, the redirect URL should be handled as response here - Bootstrap::initializeBackendAuthentication($this->isLoggedInBackendUserRequired($pathToRoute)); + $GLOBALS['BE_USER']->backendCheckLogin($this->isLoggedInBackendUserRequired($pathToRoute)); $GLOBALS['LANG'] = LanguageService::createFromUserPreferences($GLOBALS['BE_USER']); // Register the backend user as aspect $this->setBackendUserAspect($GLOBALS['BE_USER']); - return $handler->handle($request); + $response = $handler->handle($request); + + // Additional headers to never cache any PHP request should be sent at any time when + // accessing the TYPO3 Backend + return $this->applyHeadersToResponse($response); } /** @@ -92,15 +85,4 @@ class BackendUserAuthenticator implements MiddlewareInterface { return in_array($routePath, $this->publicRoutes, true); } - - /** - * Register the backend user as aspect - * - * @param BackendUserAuthentication $user - */ - protected function setBackendUserAspect(BackendUserAuthentication $user) - { - $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user)); - $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user->workspace)); - } } diff --git a/typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php new file mode 100644 index 0000000000000000000000000000000000000000..fbd1a0321aa2db2ecefb369b197a552ec6235c3c --- /dev/null +++ b/typo3/sysext/core/Classes/Middleware/BackendUserAuthenticator.php @@ -0,0 +1,97 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\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\Utility\GeneralUtility; + +/** + * Boilerplate to authenticate a backend user in the current workflow, can be used + * for TYPO3 Backend and Frontend requests. + * + * The actual authentication and the selection if no-cache headers to responses should + * be applied should still reside in the "process()" method which should be + * extended by derivative classes. + * + * In derivative classes, the Context API can be used to detect, if a backend user is logged in + * like this: + * + * $response = $handler->handle($request); + * if ($this->context->getAspect('backend.user')->isLoggedIn()) { + * return $this->applyHeadersToResponse($response); + * } + * + * @internal this class might get merged again with the subclasses + */ +abstract class BackendUserAuthenticator implements MiddlewareInterface +{ + /** + * @var Context + */ + protected $context; + + public function __construct(Context $context) + { + $this->context = $context; + } + + /** + * @inheritDoc + */ + abstract public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface; + + /** + * Adding headers to the response to avoid caching on the client side. + * These headers will override any previous headers of these names sent. + * Get the http headers to be sent if an authenticated user is available, + * in order to disallow browsers to store the response on the client side. + * + * @param ResponseInterface $response + * @return ResponseInterface the modified response object. + */ + protected function applyHeadersToResponse(ResponseInterface $response): ResponseInterface + { + $headers = [ + 'Expires' => 0, + 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT', + 'Cache-Control' => 'no-cache, must-revalidate', + // HTTP 1.0 compatibility, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma + 'Pragma' => 'no-cache' + ]; + foreach ($headers as $headerName => $headerValue) { + $response = $response->withHeader($headerName, (string)$headerValue); + } + return $response; + } + + /** + * Register the backend user as aspect + * + * @param BackendUserAuthentication|null $user + */ + protected function setBackendUserAspect(?BackendUserAuthentication $user): void + { + $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user)); + $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user ? $user->workspace : 0)); + } +} diff --git a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php index ea7eb5349bce4f2adc037d094f40d6deca0276a6..8a5eb2f6682e0eb2839e2f0b528041f289f31f72 100644 --- a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php @@ -18,13 +18,9 @@ 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\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\Localization\LanguageService; @@ -38,18 +34,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * 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 implements MiddlewareInterface +class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAuthenticator { - /** - * @var Context - */ - protected $context; - - public function __construct(Context $context) - { - $this->context = $context; - } - /** * Creates a backend user authentication object, tries to authenticate a user * @@ -77,7 +63,13 @@ class BackendUserAuthenticator implements MiddlewareInterface $this->setBackendUserAspect($GLOBALS['BE_USER']); } - return $handler->handle($request); + $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; } /** @@ -123,15 +115,4 @@ class BackendUserAuthenticator implements MiddlewareInterface } return $user->backendCheckLogin(); } - - /** - * Register the backend user as aspect - * - * @param BackendUserAuthentication|null $user - */ - protected function setBackendUserAspect(BackendUserAuthentication $user) - { - $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user)); - $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user->workspace)); - } } diff --git a/typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php b/typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..200ec497bd8827de94308ac6068e2083042114ee --- /dev/null +++ b/typo3/sysext/frontend/Tests/Functional/Middleware/BackendUserAuthenticatorTest.php @@ -0,0 +1,73 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\Tests\Functional\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 TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class BackendUserAuthenticatorTest extends FunctionalTestCase +{ + use SiteBasedTestTrait; + + protected const LANGUAGE_PRESETS = [ + 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''], + ]; + + protected function setUp(): void + { + parent::setUp(); + $this->importDataSet('EXT:core/Tests/Functional/Fixtures/pages.xml'); + $this->setUpBackendUserFromFixture(1); + $this->writeSiteConfiguration( + 'acme-com', + $this->buildSiteConfiguration(1, 'https://acme.com/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/'), + ] + ); + } + + /** + * @test + */ + public function nonAuthenticatedRequestDoesNotSendHeaders(): void + { + $response = $this->executeFrontendRequest( + (new InternalRequest('/'))->withPageId(1), + (new InternalRequestContext()) + ); + self::assertArrayNotHasKey('Cache-Control', $response->getHeaders()); + self::assertArrayNotHasKey('Pragma', $response->getHeaders()); + self::assertArrayNotHasKey('Expires', $response->getHeaders()); + } + + /** + * @test + */ + public function authenticatedRequestIncludesInvalidCacheHeaders(): void + { + $response = $this->executeFrontendRequest( + (new InternalRequest('/'))->withPageId(1), + (new InternalRequestContext()) + ->withBackendUserId(1) + ); + self::assertEquals('no-cache, must-revalidate', $response->getHeaders()['Cache-Control'][0]); + self::assertEquals('no-cache', $response->getHeaders()['Pragma'][0]); + self::assertEquals(0, $response->getHeaders()['Expires'][0]); + } +}