diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 7453ce921de5fa6d9f6de148f93e9258c67233dd..320f276bd25f8c6ac626437f21f26ff07e4dc29d 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -88,6 +88,7 @@ namespace PHPSTORM_META { 'moduleData', 'frontend.controller', 'frontend.typoscript', + 'frontend.cache.instruction', ); override(\Psr\Http\Message\ServerRequestInterface::getAttribute(), map([ 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, @@ -99,6 +100,7 @@ namespace PHPSTORM_META { 'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class, 'frontend.controller' => \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class, 'frontend.typoscript' => \TYPO3\CMS\Core\TypoScript\FrontendTypoScript::class, + 'frontend.cache.instruction' => \TYPO3\CMS\Frontend\Cache\CacheInstruction::class, ])); expectedArguments( diff --git a/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php b/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php index 85c86ac43c228206ca7b0500d49684c94d7f487e..5163f340b4afc1b4b86f59be7e4832af08b9282a 100644 --- a/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php +++ b/typo3/sysext/adminpanel/Classes/Modules/CacheModule.php @@ -26,6 +26,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; class CacheModule extends AbstractModule implements PageSettingsProviderInterface, RequestEnricherInterface, ResourceProviderInterface { @@ -84,7 +85,9 @@ class CacheModule extends AbstractModule implements PageSettingsProviderInterfac public function enrich(ServerRequestInterface $request): ServerRequestInterface { if ($this->configurationService->getConfigurationOption('cache', 'noCache')) { - $request = $request->withAttribute('noCache', true); + $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $cacheInstruction->disableCache('EXT:adminpanel: "No caching" disables cache.'); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); } return $request; } diff --git a/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php b/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php index a6b53f05a3ecd0685dfa9a14c6b44ad5f2ab9009..30cfce1375b8cf9eab26c17f381a246689e2a47b 100644 --- a/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php +++ b/typo3/sysext/adminpanel/Classes/Modules/Debug/PageTitle.php @@ -24,7 +24,6 @@ use TYPO3\CMS\Adminpanel\ModuleApi\DataProviderInterface; use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\View\StandaloneView; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Admin Panel Page Title module for showing the Page title providers @@ -62,13 +61,12 @@ class PageTitle extends AbstractSubModule implements DataProviderInterface $data = [ 'cacheEnabled' => true, ]; - if ($this->isNoCacheEnabled()) { + if (!$this->isCachingAllowed($request)) { $data = [ 'orderedProviders' => [], 'usedProvider' => null, 'skippedProviders' => [], ]; - $logRecords = GeneralUtility::makeInstance(InMemoryLogWriter::class)->getLogEntries(); foreach ($logRecords as $logEntry) { if ($logEntry->getComponent() === self::LOG_COMPONENT) { @@ -100,13 +98,8 @@ class PageTitle extends AbstractSubModule implements DataProviderInterface return $view->render(); } - protected function isNoCacheEnabled(): bool - { - return (bool)$this->getTypoScriptFrontendController()->no_cache; - } - - protected function getTypoScriptFrontendController(): TypoScriptFrontendController + protected function isCachingAllowed(ServerRequestInterface $request): bool { - return $GLOBALS['TSFE']; + return $request->getAttribute('frontend.cache.instruction')->isCachingAllowed(); } } diff --git a/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php b/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php index 91e7dbd6ef2aaca6dfde9a536efb2911b77908a9..c3f0c0f51c343d26dbd7cb919e377abe24d8b651 100644 --- a/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php +++ b/typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php @@ -53,15 +53,16 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf 'pageUid' => $tsfe->id, 'pageType' => $tsfe->getPageArguments()->getPageType(), 'groupList' => implode(',', $frontendUserAspect->getGroupIds()), - 'noCache' => $this->isNoCacheEnabled(), + 'noCache' => !$this->isCachingAllowed($request), + 'noCacheReasons' => $request->getAttribute('frontend.cache.instruction')->getDisabledCacheReasons(), 'countUserInt' => count($tsfe->config['INTincScript'] ?? []), 'totalParsetime' => $this->getTimeTracker()->getParseTime(), 'feUser' => [ 'uid' => $frontendUserAspect->get('id') ?: 0, 'username' => $frontendUserAspect->get('username') ?: '', ], - 'imagesOnPage' => $this->collectImagesOnPage(), - 'documentSize' => $this->collectDocumentSize(), + 'imagesOnPage' => $this->collectImagesOnPage($request), + 'documentSize' => $this->collectDocumentSize($request), ], ] ); @@ -106,7 +107,7 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf * Collects images from TypoScriptFrontendController and calculates the total size. * Returns human-readable image sizes for fluid template output */ - protected function collectImagesOnPage(): array + protected function collectImagesOnPage(ServerRequestInterface $request): array { $imagesOnPage = [ 'files' => [], @@ -115,7 +116,7 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf 'totalSizeHuman' => GeneralUtility::formatSize(0), ]; - if ($this->isNoCacheEnabled() === false) { + if ($this->isCachingAllowed($request)) { return $imagesOnPage; } @@ -138,21 +139,20 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf } /** - * Gets the document size from the current page in a human readable format + * Gets the document size from the current page in a human-readable format */ - protected function collectDocumentSize(): string + protected function collectDocumentSize(ServerRequestInterface $request): string { $documentSize = 0; - if ($this->isNoCacheEnabled() === true) { + if (!$this->isCachingAllowed($request)) { $documentSize = mb_strlen($this->getTypoScriptFrontendController()->content, 'UTF-8'); } - return GeneralUtility::formatSize($documentSize); } - protected function isNoCacheEnabled(): bool + protected function isCachingAllowed(ServerRequestInterface $request): bool { - return (bool)$this->getTypoScriptFrontendController()->no_cache; + return $request->getAttribute('frontend.cache.instruction')->isCachingAllowed(); } protected function getTypoScriptFrontendController(): TypoScriptFrontendController diff --git a/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php b/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php index d6d7688556cba189d7fde45e6eb7ce666867ca94..ed8ebbf3a9b532c3203c1dd8ba20f7b2cd9e1401 100644 --- a/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php +++ b/typo3/sysext/adminpanel/Classes/Modules/PreviewModule.php @@ -37,6 +37,7 @@ use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\View\StandaloneView; use TYPO3\CMS\Frontend\Aspect\PreviewAspect; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; /** * Admin Panel Preview Module @@ -45,8 +46,6 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface, { use LoggerAwareTrait; - protected CacheManager $cacheManager; - /** * module configuration, set on initialize * @@ -61,10 +60,9 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface, */ protected array $config; - public function injectCacheManager(CacheManager $cacheManager): void - { - $this->cacheManager = $cacheManager; - } + public function __construct( + protected readonly CacheManager $cacheManager, + ) {} public function getIconIdentifier(): string { @@ -98,8 +96,11 @@ class PreviewModule extends AbstractModule implements RequestEnricherInterface, ]; if ($this->config['showFluidDebug']) { // forcibly unset fluid caching as it does not care about the tsfe based caching settings + // @todo: Useless?! CacheManager is initialized via bootstrap already, TYPO3_CONF_VARS is not read again? unset($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['fluid_template']['frontend']); - $request = $request->withAttribute('noCache', true); + $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $cacheInstruction->disableCache('EXT:adminpanel: "Show fluid debug output" disables cache.'); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); } $this->initializeFrontendPreview( $this->config['showHiddenPages'], diff --git a/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php b/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php index 50cc86c43b86273b26a64f3d64e20f8261d7edaf..1add7bd0c289e3812266cab6a72d3424bb10f3fe 100644 --- a/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php +++ b/typo3/sysext/adminpanel/Classes/Modules/TsDebug/TypoScriptWaterfall.php @@ -26,6 +26,7 @@ use TYPO3\CMS\Adminpanel\Service\ConfigurationService; use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; /** * Class TypoScriptWaterfall @@ -53,7 +54,9 @@ class TypoScriptWaterfall extends AbstractSubModule implements RequestEnricherIn public function enrich(ServerRequestInterface $request): ServerRequestInterface { if ($this->getConfigurationOption('forceTemplateParsing')) { - $request = $request->withAttribute('noCache', true); + $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $cacheInstruction->disableCache('EXT:adminpanel: "Force TS rendering" disables cache.'); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); } $this->getTimeTracker()->LR = $this->getConfigurationOption('LR'); return $request; diff --git a/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html b/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html index e3f8b1cf767b8d20779674246accadea780809a9..6ce5aa7b025e41fe735efe8d0ab4cd28df6dd1ed 100644 --- a/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html +++ b/typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Info/General.html @@ -74,6 +74,10 @@ \'LLL:EXT:adminpanel/Resources/Private/Language/locallang_info.xlf:noCache\': isCachedInfo }'}" debug="false"/> +<f:if condition="{info.noCache}"> + <f:render partial="Data/TableKeyValue" arguments="{label: 'Disabled Cache reasons', languageKey: languageKey, data: info.noCacheReasons}" debug="false"/> +</f:if> + <f:render partial="Data/TableKeyValue" arguments="{label: 'UserIntObjects', languageKey: languageKey, data: '{ \'LLL:EXT:adminpanel/Resources/Private/Language/locallang_info.xlf:countUserInt\': info.countUserInt }'}" debug="false"/> diff --git a/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php b/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php index 1b6e361eab4af17783e29c7f515c0dd7e64891f7..0e1e539ec5e549b1ee6f2b936b96f1b077d9a291 100644 --- a/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php +++ b/typo3/sysext/adminpanel/Tests/Unit/Modules/PreviewModuleTest.php @@ -19,6 +19,7 @@ namespace TYPO3\CMS\Adminpanel\Tests\Unit\Modules; use TYPO3\CMS\Adminpanel\Modules\PreviewModule; use TYPO3\CMS\Adminpanel\Service\ConfigurationService; +use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -63,7 +64,7 @@ final class PreviewModuleTest extends UnitTestCase ]; $configurationService->method('getConfigurationOption')->withAnyParameters()->willReturnMap($valueMap); - $previewModule = new PreviewModule(); + $previewModule = new PreviewModule($this->createMock(CacheManager::class)); $previewModule->injectConfigurationService($configurationService); $previewModule->enrich(new ServerRequest()); @@ -102,7 +103,7 @@ final class PreviewModuleTest extends UnitTestCase }); GeneralUtility::setSingletonInstance(Context::class, $context); - $previewModule = new PreviewModule(); + $previewModule = new PreviewModule($this->createMock(CacheManager::class)); $previewModule->injectConfigurationService($configurationService); $previewModule->enrich($request); } diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst index 4a903d722065ddd723a4964b808200999d39e31b..d100bf6f466df6dcc713fe6b5de9867e136d5037 100644 --- a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst +++ b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst @@ -59,6 +59,7 @@ The following public class properties have been marked "read only": The following public class properties have been marked :php:`@internal` - in general all properties not listed above: +* :php:`TypoScriptFrontendController->no_cache` - Use Request attribute :php:`frontend.cache.instruction` instead * :php:`TypoScriptFrontendController->additionalHeaderData` * :php:`TypoScriptFrontendController->additionalFooterData` * :php:`TypoScriptFrontendController->register` diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102628-CacheInstructionMiddleware.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102628-CacheInstructionMiddleware.rst new file mode 100644 index 0000000000000000000000000000000000000000..45b33153b41d7210d42f1bb27a11515f5b737c5b --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102628-CacheInstructionMiddleware.rst @@ -0,0 +1,42 @@ +.. include:: /Includes.rst.txt + +.. _feature-102628-1702031683: + +=============================================== +Feature: #102628 - Cache instruction middleware +=============================================== + +See :issue:`102628` + +Description +=========== + +TYPO3 v13 introduces the new Frontend related Request attribute :php`frontend.cache.instruction` +implemented by class :php:`TYPO3\CMS\Frontend\Cache\CacheInstruction`. This replaces the +previous :php:`TyposcriptFrontendController->no_cache` property and boolean php:`noCache` Request +attribute. + +Impact +====== + +The attribute can be used by middlewares to disable cache mechanics of the Frontend rendering. + +In early middlewares before :php:`typo3/cms-frontend/tsfe`, the attribute may or may not exist +already. A safe way to interact with it is like this: + +.. code-block:: php + + $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $cacheInstruction->disableCache('EXT:my-extension: My-reason disables caches.'); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); + +Extension with middlewares or other code after :php:`typo3/cms-frontend/tsfe` can assume the attribute to +be set already. Usage example: + +.. code-block:: php + + $cacheInstruction = $request->getAttribute('frontend.cache.instruction'); + $cacheInstruction->disableCache('EXT:my-extension: My-reason disables caches.'); + + +.. index:: Frontend, PHP-API, ext:frontend diff --git a/typo3/sysext/frontend/Classes/Cache/CacheInstruction.php b/typo3/sysext/frontend/Classes/Cache/CacheInstruction.php new file mode 100644 index 0000000000000000000000000000000000000000..87c2338233f83dce2cbf5db045912e5db1b7addd --- /dev/null +++ b/typo3/sysext/frontend/Classes/Cache/CacheInstruction.php @@ -0,0 +1,71 @@ +<?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\Cache; + +/** + * This class contains cache details and is created or updated in middlewares of the + * Frontend rendering chain and added as Request attribute "frontend.cache.instruction". + * + * Its main goal is to *disable* the Frontend cache mechanisms in various scenarios, for + * instance when the admin panel is used to simulate access times, or when security + * mechanisms like cHash evaluation do not match. + */ +final class CacheInstruction +{ + private bool $allowCaching = true; + private array $disabledCacheReasons = []; + + /** + * Instruct the core Frontend rendering to disable Frontend caching. Extensions with + * custom middlewares may set this. + * + * Note multiple cache layers are involved during Frontend rendering: For instance multiple + * TypoScript layers, the page cache and potentially others. Those caches are read from and + * written to within various middlewares. Depending on the position of a call to this method + * within the middleware stack, it can happen that some or all caches have already been + * read of written. + * + * Extensions that use this method should keep an eye on their middleware positions in the + * stack to estimate the performance impact of this call. It's of course best to not use + * the 'disable cache' mechanic at all, but to handle caching properly in extensions. + */ + public function disableCache(string $reason): void + { + if (empty($reason)) { + throw new \RuntimeException( + 'A non-empty reason must be given to disable cache. At least mention the extension name that triggers it.', + 1701528694 + ); + } + $this->allowCaching = false; + $this->disabledCacheReasons[] = $reason; + } + + public function isCachingAllowed(): bool + { + return $this->allowCaching; + } + + /** + * @internal Typically only consumed by extensions like EXT:adminpanel + */ + public function getDisabledCacheReasons(): array + { + return $this->disabledCacheReasons; + } +} diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php index 038f0d57c24e4129cc3afc4a71696c2885d104b6..60186e970edfb48a5ab59712eac6c7b6bd757d9a 100644 --- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php +++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php @@ -689,7 +689,7 @@ class ContentObjectRenderer implements LoggerAwareInterface } // Store cache - if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) { + if ($cacheConfiguration !== null && $this->getRequest()->getAttribute('frontend.cache.instruction')->isCachingAllowed()) { $key = $this->calculateCacheKey($cacheConfiguration); if (!empty($key)) { $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash'); @@ -5497,7 +5497,7 @@ class ContentObjectRenderer implements LoggerAwareInterface */ protected function getFromCache(array $configuration) { - if ($this->getTypoScriptFrontendController()->no_cache) { + if (!$this->getRequest()->getAttribute('frontend.cache.instruction')->isCachingAllowed()) { return false; } $cacheKey = $this->calculateCacheKey($configuration); diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index 7bcc7e742f638c75b7f9ca5538289489170806a4..65a6b7991cba38f84e39d1d5190e976444dd1356 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -74,7 +74,6 @@ use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Core\Utility\RootlineUtility; -use TYPO3\CMS\Frontend\Aspect\PreviewAspect; use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; @@ -113,14 +112,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface protected SiteLanguage $language; protected PageArguments $pageArguments; - /** - * Page will not be cached. Write only TRUE. Never clear value (some other - * code might have reasons to set it TRUE). - * @var bool - * @internal - */ - public $no_cache = false; - /** * Rootline of page records all the way to the root. * @@ -399,7 +390,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ public function __construct(Context $context, Site $site, SiteLanguage $siteLanguage, PageArguments $pageArguments) { - $this->initializeContext($context); + $this->context = $context; $this->site = $site; $this->language = $siteLanguage; $this->setPageArguments($pageArguments); @@ -408,14 +399,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->initCaches(); } - private function initializeContext(Context $context): void - { - $this->context = $context; - if (!$this->context->hasAspect('frontend.preview')) { - $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class)); - } - } - protected function initPageRenderer(): void { if ($this->pageRenderer !== null) { @@ -870,7 +853,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface } /** - * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions. That data can be used inside a page-not-found handler + * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions. + * That data can be used inside a page-not-found handler * * @param string|null $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details * @return array Summary of why page access was not allowed. @@ -890,7 +874,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class); foreach ($combinedRecords as $k => $pagerec) { // If $k=0 then it is the very first page the original ID was pointing at and that will get a full check of course - // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the extendToSubpages flag set, hence checked only then! + // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the + // extendToSubpages flag set, hence checked only then! if (!$k || $pagerec['extendToSubpages']) { if ($pagerec['hidden'] ?? false) { $output['hidden'][$pagerec['uid']] = true; @@ -1009,6 +994,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface } $site = $this->getSite(); + $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed(); $tokenizer = new LossyTokenizer(); $treeBuilder = GeneralUtility::makeInstance(SysTemplateTreeBuilder::class); @@ -1017,9 +1003,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface $cacheManager = GeneralUtility::makeInstance(CacheManager::class); /** @var PhpFrontend|null $typoscriptCache */ $typoscriptCache = null; - if (!$this->no_cache) { - // $this->no_cache = true might have been set by earlier TypoScriptFrontendInitialization middleware. - // This means we don't do fancy cache stuff, calculate full TypoScript and ignore page cache. + if ($isCachingAllowed) { + // disableCache() might have been called by earlier middlewares. This means we don't do fancy cache + // stuff, calculate full TypoScript and don't get() from nor set() to typoscript and page cache. /** @var PhpFrontend|null $typoscriptCache */ $typoscriptCache = $cacheManager->getCache('typoscript'); } @@ -1048,7 +1034,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $flatConstants = []; $serializedConstantConditionList = ''; $gotConstantFromCache = false; - if (!$this->no_cache && $constantConditionIncludeTree = $typoscriptCache->require($constantConditionIncludeListCacheIdentifier)) { + if ($isCachingAllowed && $constantConditionIncludeTree = $typoscriptCache->require($constantConditionIncludeListCacheIdentifier)) { // We got the flat list of all constants conditions for this TypoScript combination from cache. Good. We traverse // this list to calculate "current" condition verdicts. With a hash of this list together with a hash of the // TypoScript sys_templates, we try to retrieve the full constants TypoScript from cache. @@ -1068,12 +1054,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface $gotConstantFromCache = true; } } - if ($this->no_cache || !$gotConstantFromCache) { + if (!$isCachingAllowed || !$gotConstantFromCache) { // We did not get constants from cache, or are not allowed to use cache. We have to build constants from scratch. // This means we'll fetch the full constants include tree (from cache if possible), register the condition // matcher and register the AST builder and traverse include tree to retrieve constants AST and calculate // 'flat constants' from it. Both are cached if allowed afterwards for the 'if' above to kick in next time. - // $typoscriptCache can be null here with no_cache=1. $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $tokenizer, $site, $typoscriptCache); $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); @@ -1085,12 +1070,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface // children for not matching conditions, which is important to create the correct AST. $includeTreeTraverserConditionVerdictAware->traverse($constantIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors); $constantsAst = $constantAstBuilderVisitor->getAst(); - // @internal Dispatch and experimental event allowing listeners to still change the constants AST, + // @internal Dispatch an experimental event allowing listeners to still change the constants AST, // to for instance implement nested constants if really needed. Note this event may change // or vanish later without further notice. $constantsAst = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(new ModifyTypoScriptConstantsEvent($constantsAst))->getConstantsAst(); $flatConstants = $constantsAst->flatten(); - if (!$this->no_cache) { + if ($isCachingAllowed) { // We are allowed to cache and can create both the full list of conditions, plus the constant AST and flat constant // list cache entry. To do that, we need all (!) conditions, but the above ConditionVerdictAwareIncludeTreeTraverser // did not find nested conditions if an upper condition did not match. We thus have to traverse include tree a @@ -1121,7 +1106,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $setupConditionIncludeListCacheIdentifier = 'setup-condition-include-list-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList); $setupConditionList = []; $gotSetupConditionsFromCache = false; - if (!$this->no_cache && $setupConditionIncludeTree = $typoscriptCache->require($setupConditionIncludeListCacheIdentifier)) { + if ($isCachingAllowed && $setupConditionIncludeTree = $typoscriptCache->require($setupConditionIncludeListCacheIdentifier)) { // We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse // this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier. $includeTreeTraverserVisitors = []; @@ -1138,12 +1123,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface $gotSetupConditionsFromCache = true; } $setupIncludeTree = null; - if ($this->no_cache || !$gotSetupConditionsFromCache) { + if (!$isCachingAllowed || !$gotSetupConditionsFromCache) { // We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup // condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible), // register the constant substitution visitor, and register condition matcher and register the condition // accumulator visitor. - // $typoscriptCache can be null here with no_cache=1. $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache); $includeTreeTraverserVisitors = []; $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor(); @@ -1167,7 +1151,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface // obviously the page id. $this->lock = GeneralUtility::makeInstance(ResourceMutex::class); $this->newHash = $this->createHashBase($sysTemplateRows, $constantConditionList, $setupConditionList); - if (!$this->no_cache) { + if ($isCachingAllowed) { if ($this->shouldAcquireCacheData($request)) { // Try to get a page cache row. $this->getTimeTracker()->push('Cache Row'); @@ -1213,16 +1197,15 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->lock->acquireLock('pages', $this->newHash); } - if ($this->no_cache || empty($this->config) || $this->isINTincScript()) { - // We don't need the full setup AST in many cached scenarios. However, if no_cache is set, if no page cache - // entry could be loaded, if the page cache entry has _INT object, or if the user forced template - // parsing (adminpanel), then we still need the full setup AST. If there is "just" an _INT object, we can - // use a possible cache entry for the setup AST, which speeds up _INT parsing quite a bit. In other cases - // we calculate full setup AST and cache it if allowed. + if (!$isCachingAllowed || empty($this->config) || $this->isINTincScript()) { + // We don't need the full setup AST in many cached scenarios. However, if caching is not allowed, if no page + // cache entry could be loaded or if the page cache entry has _INT object, then we still need the full setup AST. + // If there is "just" an _INT object, we can use a possible cache entry for the setup AST, which speeds up _INT + // parsing quite a bit. In other cases we calculate full setup AST and cache it if allowed. $setupTypoScriptCacheIdentifier = 'setup-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList . serialize($setupConditionList)); $gotSetupFromCache = false; $setupArray = []; - if (!$this->no_cache) { + if ($isCachingAllowed) { // We need AST, but we are allowed to potentially get it from cache. if ($setupTypoScriptCache = $typoscriptCache->require($setupTypoScriptCacheIdentifier)) { $frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']); @@ -1230,11 +1213,10 @@ class TypoScriptFrontendController implements LoggerAwareInterface $gotSetupFromCache = true; } } - if ($this->no_cache || !$gotSetupFromCache) { + if (!$isCachingAllowed || !$gotSetupFromCache) { // We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup // IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not. - if ($this->no_cache || $setupIncludeTree === null) { - // $typoscriptCache can be null here with no_cache=1. + if (!$isCachingAllowed || $setupIncludeTree === null) { $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache); } $includeTreeTraverserConditionVerdictAwareVisitors = []; @@ -1279,7 +1261,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $setupAst->addChild($typesNode); } $setupArray = $setupAst->toArray(); - if (!$this->no_cache) { + if ($isCachingAllowed) { // Write cache entry for AST and its array representation, we're allowed to do it. $typoscriptCache->set($setupTypoScriptCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupArray]), '\'\\') . '\');'); } @@ -1322,9 +1304,10 @@ class TypoScriptFrontendController implements LoggerAwareInterface $frontendTypoScript->setSetupArray($setupArray); } - // Set $this->no_cache TRUE if the config.no_cache value is set! - if (!$this->no_cache && ($this->config['config']['no_cache'] ?? false)) { - $this->set_no_cache('config.no_cache is set', true); + // Disable cache if config.no_cache is set! + if ($this->config['config']['no_cache'] ?? false) { + $cacheInstruction = $request->getAttribute('frontend.cache.instruction'); + $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"'); } // Auto-configure settings when a site is configured @@ -1389,8 +1372,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ protected function shouldAcquireCacheData(ServerRequestInterface $request): bool { - // Trigger event for possible by-pass of requiring of page cache (for re-caching purposes) - $event = new ShouldUseCachedPageDataIfAvailableEvent($request, $this, !$this->no_cache); + // Trigger event for possible by-pass of requiring of page cache. + $event = new ShouldUseCachedPageDataIfAvailableEvent($request, $this, $request->getAttribute('frontend.cache.instruction')->isCachingAllowed()); GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event); return $event->shouldUseCachedPageData(); } @@ -1812,7 +1795,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface { $this->setAbsRefPrefix(); $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); - $event = new AfterCacheableContentIsGeneratedEvent($request, $this, $this->newHash, !$this->no_cache); + $usePageCache = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed(); + $event = new AfterCacheableContentIsGeneratedEvent($request, $this, $this->newHash, $usePageCache); $event = $eventDispatcher->dispatch($event); // Processing if caching is enabled @@ -2070,7 +2054,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface * * @internal */ - public function applyHttpHeadersToResponse(ResponseInterface $response): ResponseInterface + public function applyHttpHeadersToResponse(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $response = $response->withHeader('Content-Type', $this->contentType); // Set header for content language unless disabled @@ -2085,7 +2069,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface } // Set cache related headers to client (used to enable proxy / client caching!) - $headers = $this->getCacheHeaders(); + $headers = $this->getCacheHeaders($request); foreach ($headers as $header => $value) { $response = $response->withHeader($header, $value); } @@ -2108,11 +2092,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface /** * Get cache headers good for client/reverse proxy caching. */ - protected function getCacheHeaders(): array + protected function getCacheHeaders(ServerRequestInterface $request): array { $headers = []; // Getting status whether we can send cache control headers for proxy caching: - $doCache = $this->isStaticCacheble(); + $doCache = $this->isStaticCacheble($request); $isBackendUserLoggedIn = $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false); $isInWorkspace = $this->context->getPropertyFromAspect('workspace', 'isOffline', false); // Finally, when backend users are logged in, do not send cache headers at all (Admin Panel might be displayed for instance). @@ -2139,8 +2123,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->getTimeTracker()->setTSlogMessage('Cache-headers with max-age "' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']) . '" would have been sent'); } else { $reasonMsg = []; - if ($this->no_cache) { - $reasonMsg[] = 'Caching disabled (no_cache).'; + if (!$request->getAttribute('frontend.cache.instruction')->isCachingAllowed()) { + $reasonMsg[] = 'Caching disabled.'; } if ($this->isINTincScript()) { $reasonMsg[] = '*_INT object(s) on page.'; @@ -2169,9 +2153,10 @@ class TypoScriptFrontendController implements LoggerAwareInterface * * @internal */ - public function isStaticCacheble(): bool + public function isStaticCacheble(ServerRequestInterface $request): bool { - return !$this->no_cache && !$this->isINTincScript() && !$this->context->getAspect('frontend.user')->isUserOrGroupSet(); + $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed(); + return $isCachingAllowed && !$this->isINTincScript() && !$this->context->getAspect('frontend.user')->isUserOrGroupSet(); } /** @@ -2258,9 +2243,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface * Sets the cache-flag to 1. Could be called from user-included php-files in order to ensure that a page is not cached. * * @param string $reason An optional reason to be written to the log. - * @param bool $internalRequest Whether the request is internal or not (true should only be used by core calls). + * @todo: deprecate */ - public function set_no_cache(string $reason = '', bool $internalRequest = false): void + public function set_no_cache(string $reason = ''): void { $warning = ''; $context = []; @@ -2284,24 +2269,19 @@ class TypoScriptFrontendController implements LoggerAwareInterface } $context['line'] = $trace[0]['line']; } - if (!$internalRequest && $GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']) { + if ($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']) { $warning .= ' However, $TYPO3_CONF_VARS[\'FE\'][\'disableNoCacheParameter\'] is set, so it will be ignored!'; $this->getTimeTracker()->setTSlogMessage($warning, LogLevel::NOTICE); } else { $warning .= ' Caching is disabled!'; - $this->disableCache(); + /** @var ServerRequestInterface $request */ + $request = $GLOBALS['TYPO3_REQUEST']; + $cacheInstruction = $request->getAttribute('frontend.cache.instruction'); + $cacheInstruction->disableCache('EXT:frontend: Caching disabled using deprecated set_no_cache().'); } $this->logger->notice($warning, $context); } - /** - * Disables caching of the current page. - */ - protected function disableCache(): void - { - $this->no_cache = true; - } - /** * Sets the cache-timeout in seconds * diff --git a/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php index 77a34659fc6370932220abe22dca68f14f833810..22c89be2f0ebe36d22d4162e79715863eb8173ce 100644 --- a/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php +++ b/typo3/sysext/frontend/Classes/Event/AfterCacheableContentIsGeneratedEvent.php @@ -21,7 +21,7 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** - * Event that allows to enhance or change content (also depending if caching is enabled). + * Event that allows to enhance or change content (also depending on enabled caching). * Think of $this->isCachingEnabled() as the same as $TSFE->no_cache. * Depending on disable or enabling caching, the cache is then not stored in the pageCache. */ diff --git a/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php index 18543c1a6a03131b954250481b65cacab2527af8..fd77aded595dc245c1039a40042b00449a88c89a 100644 --- a/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php +++ b/typo3/sysext/frontend/Classes/Event/AfterCachedPageIsPersistedEvent.php @@ -21,12 +21,12 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** - * Event that is used directly after all cached content is stored in - * the page cache. + * Event that is used directly after all cached content is stored in the page cache. * - * If a page is called from the cache, this event is NOT fired. - * This event is also NOT FIRED when $TSFE->no_cache (or manipulated via AfterCacheableContentIsGeneratedEvent) - * is set. + * NOT fired, if: + * * A page is called from the cache + * * Caching is disabled using 'frontend.cache.instruction' request attribute, which can + * be set by various middlewares or AfterCacheableContentIsGeneratedEvent */ final class AfterCachedPageIsPersistedEvent { diff --git a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php index ba58f88dab7af601d707d14e5cb47d54d155f3b1..b5602b3d7f2db76b3d94325d71a38cc790f85f8c 100644 --- a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php +++ b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php @@ -22,7 +22,7 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Event to allow listeners to disable the loading of cached page data when a page is requested. - * Does not have any effect if "no_cache" is activated, or if there is no cached version of a page. + * Does not have any effect if caching is disabled, or if there is no cached version of a page. */ final class ShouldUseCachedPageDataIfAvailableEvent { diff --git a/typo3/sysext/frontend/Classes/Http/RequestHandler.php b/typo3/sysext/frontend/Classes/Http/RequestHandler.php index 4624459a6264087137b4499e06f298df07f1b217..84618a74ba4e14acd76434e5a4c080b9f3b28fb9 100644 --- a/typo3/sysext/frontend/Classes/Http/RequestHandler.php +++ b/typo3/sysext/frontend/Classes/Http/RequestHandler.php @@ -179,7 +179,7 @@ class RequestHandler implements RequestHandlerInterface // Create a default Response object and add headers and body to it $response = new Response(); - $response = $controller->applyHttpHeadersToResponse($response); + $response = $controller->applyHttpHeadersToResponse($request, $response); $this->displayPreviewInfoMessage($controller); $response->getBody()->write($controller->content); return $response; diff --git a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php index dd2ee67f000ab2fffbb9e9e60f8eb84104fbe163..039b06befcff207a37f846bcd1ddbe4a3d980f5b 100644 --- a/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php +++ b/typo3/sysext/frontend/Classes/Middleware/BackendUserAuthenticator.php @@ -28,6 +28,7 @@ use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Localization\LanguageServiceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; /** * This middleware authenticates a Backend User (be_user) (pre)-viewing a frontend page. @@ -70,9 +71,11 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut && (strtolower($request->getServerParams()['HTTP_CACHE_CONTROL'] ?? '') === 'no-cache' || strtolower($request->getServerParams()['HTTP_PRAGMA'] ?? '') === 'no-cache') ) { - // Detecting if shift-reload has been clicked to set noCache attribute if so. + // 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. - $request = $request->withAttribute('noCache', true); + $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); } } diff --git a/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php b/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php index 3d2b91b3b63415600cd9b9dcd7afb8ba37a8e313..ee2b25c46539eb9015cbfdab25ac4419797c9213 100644 --- a/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php +++ b/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php @@ -23,12 +23,11 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; -use Psr\Log\LogLevel; 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\Cache\CacheInstruction; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Page\CacheHashCalculator; use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; @@ -40,14 +39,8 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface { use LoggerAwareTrait; - /** - * @var bool will be used to set $TSFE->no_cache later-on - */ - protected bool $disableCache = false; - public function __construct( - protected readonly CacheHashCalculator $cacheHashCalculator, - protected readonly TimeTracker $timeTracker + private readonly CacheHashCalculator $cacheHashCalculator, ) {} /** @@ -55,10 +48,10 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $this->disableCache = (bool)$request->getAttribute('noCache', false); + $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); $pageNotFoundOnValidationError = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] ?? true); - /** @var PageArguments $pageArguments */ - $pageArguments = $request->getAttribute('routing', null); + $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 @@ -68,12 +61,12 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface ['code' => PageAccessFailureReasons::INVALID_PAGE_ARGUMENTS] ); } - if ($GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter'] ?? true) { - $cachingDisabledByRequest = false; - } else { - $cachingDisabledByRequest = $pageArguments->getArguments()['no_cache'] ?? $request->getParsedBody()['no_cache'] ?? false; + 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 (($cachingDisabledByRequest || $this->disableCache) && !$pageNotFoundOnValidationError) { + if (!$cacheInstruction->isCachingAllowed() && !$pageNotFoundOnValidationError) { // No need to test anything if caching was already disabled. return $handler->handle($request); } @@ -84,15 +77,14 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface $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 + // 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($cHash, $relevantParametersForCacheHashArgument, $pageNotFoundOnValidationError)) { + if (!$this->evaluateCacheHashParameter($cacheInstruction, $cHash, $relevantParametersForCacheHashArgument, $pageNotFoundOnValidationError)) { return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( $request, 'Request parameters could not be validated (&cHash comparison failed)', @@ -100,7 +92,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface ); } // No cHash given but was required - } elseif (!$this->evaluatePageArgumentsWithoutCacheHash($pageArguments, $pageNotFoundOnValidationError)) { + } elseif (!$this->evaluatePageArgumentsWithoutCacheHash($cacheInstruction, $pageArguments, $pageNotFoundOnValidationError)) { return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( $request, 'Request parameters could not be validated (&cHash empty)', @@ -109,7 +101,6 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface } } - $request = $request->withAttribute('noCache', $this->disableCache); return $handler->handle($request); } @@ -134,7 +125,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface * @param bool $pageNotFoundOnCacheHashError see $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] * @return bool if false, then a PageNotFound response is triggered */ - protected function evaluateCacheHashParameter(string $cHash, array $relevantParameters, bool $pageNotFoundOnCacheHashError): bool + protected function evaluateCacheHashParameter(CacheInstruction $cacheInstruction, string $cHash, array $relevantParameters, bool $pageNotFoundOnCacheHashError): bool { $calculatedCacheHash = $this->cacheHashCalculator->calculateCacheHash($relevantParameters); if (hash_equals($calculatedCacheHash, $cHash)) { @@ -145,8 +136,8 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface return false; } // Caching is disabled now (but no 404) - $this->disableCache = true; - $this->timeTracker->setTSlogMessage('The incoming cHash "' . $cHash . '" and calculated cHash "' . $calculatedCacheHash . '" did not match, so caching was disabled. The fieldlist used was "' . implode(',', array_keys($relevantParameters)) . '"', LogLevel::ERROR); + $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; } @@ -156,9 +147,8 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface * Should only be called if NO cHash parameter is given. * * @param array<string, string|array> $dynamicArguments - * @param bool $pageNotFoundOnCacheHashError */ - protected function evaluateQueryParametersWithoutCacheHash(array $dynamicArguments, bool $pageNotFoundOnCacheHashError): bool + protected function evaluateQueryParametersWithoutCacheHash(CacheInstruction $cacheInstruction, array $dynamicArguments, bool $pageNotFoundOnCacheHashError): bool { if (!$this->cacheHashCalculator->doParametersRequireCacheHash(HttpUtility::buildQueryString($dynamicArguments))) { return true; @@ -168,8 +158,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface return false; } // Caching is disabled now (but no 404) - $this->disableCache = true; - $this->timeTracker->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', LogLevel::ERROR); + $cacheInstruction->disableCache('EXT:frontend: No cHash query argument was sent for GET vars though required. Caching is disabled.'); return true; } @@ -179,11 +168,11 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface * * Is only called if NO cHash parameter is given. */ - protected function evaluatePageArgumentsWithoutCacheHash(PageArguments $pageArguments, bool $pageNotFoundOnCacheHashError): bool + protected function evaluatePageArgumentsWithoutCacheHash(CacheInstruction $cacheInstruction, PageArguments $pageArguments, bool $pageNotFoundOnCacheHashError): bool { // legacy behaviour if (!($GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['enforceValidation'] ?? false)) { - return $this->evaluateQueryParametersWithoutCacheHash($pageArguments->getDynamicArguments(), $pageNotFoundOnCacheHashError); + 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. @@ -198,8 +187,7 @@ class PageArgumentValidator implements MiddlewareInterface, LoggerAwareInterface return true; } // Caching is disabled now (but no 404) - $this->disableCache = true; - $this->timeTracker->setTSlogMessage('No &cHash parameter was sent for given query parameters, so caching is disabled', LogLevel::ERROR); + $cacheInstruction->disableCache('EXT:frontend: No cHash query argument was sent for given query parameters. Caching is disabled'); return true; } } diff --git a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php index 9a7b98b01cf387e8563e0db70e725576ece4950d..98ed0694204b1ca7ea87846484fef671cf55dfe7 100644 --- a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php +++ b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php @@ -26,6 +26,8 @@ use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Aspect\PreviewAspect; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; @@ -50,10 +52,25 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + // The cache information attribute may be set by previous middlewares already. Make sure we have one from now on. + $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); + + // Make sure frontend.preview is given from now on. + if (!$this->context->hasAspect('frontend.preview')) { + $this->context->setAspect('frontend.preview', new PreviewAspect()); + } + // If the frontend is showing a preview, caching MUST be disabled. + if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) { + // @todo: To disentangle this, the preview aspect could be dropped and middlewares that set isPreview true + // could directly set $cacheInstruction->disableCache() instead. + $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to enabled frontend.preview aspect isPreview.'); + } + $GLOBALS['TYPO3_REQUEST'] = $request; /** @var Site $site */ - $site = $request->getAttribute('site', null); - $pageArguments = $request->getAttribute('routing', null); + $site = $request->getAttribute('site'); + $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 @@ -71,17 +88,6 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface $request->getAttribute('language', $site->getDefaultLanguage()), $pageArguments ); - if ($pageArguments->getArguments()['no_cache'] ?? $request->getParsedBody()['no_cache'] ?? false) { - $controller->set_no_cache('&no_cache=1 has been supplied, so caching is disabled! URL: "' . (string)$request->getUri() . '"'); - } - // Usually only set by the PageArgumentValidator - if ($request->getAttribute('noCache', false)) { - $controller->no_cache = true; - } - // If the frontend is showing a preview, caching MUST be disabled. - if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) { - $controller->set_no_cache('Preview active', true); - } $directResponse = $controller->determineId($request); if ($directResponse) { return $directResponse; diff --git a/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php index b6a77fa28708ea80daf6733899fda5c4ef5076e6..f896a400dce9eded0b9a1ec8fe4b847aeab1a840 100644 --- a/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php +++ b/typo3/sysext/frontend/Tests/Functional/Controller/TypoScriptFrontendControllerTest.php @@ -19,6 +19,7 @@ namespace TYPO3\CMS\Frontend\Tests\Functional\Controller; use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend; use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction; use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; @@ -390,7 +391,9 @@ alert(yes);', $body); $request = (new InternalRequest('https://website.local/en/'))->withPageId($pid); if ($nocache) { - $request = $request->withAttribute('noCache', true); + $cacheInstruction = new CacheInstruction(); + $cacheInstruction->disableCache('EXT:frontend: Testing disables caching.'); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); } $this->executeFrontendSubRequest($request); self::assertSame($expectedRootLine, $GLOBALS['TSFE']->rootLine); diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php index 9404fe53bfc0a68089678a56c2293d1fb2dc3e02..79107e053ac6474f9e02e5c2bd535af980d2395b 100644 --- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php +++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php @@ -62,6 +62,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\CMS\Fluid\View\StandaloneView; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject; use TYPO3\CMS\Frontend\ContentObject\CaseContentObject; use TYPO3\CMS\Frontend\ContentObject\ContentContentObject; @@ -2709,6 +2710,7 @@ final class ContentObjectRendererTest extends UnitTestCase ContentObjectRenderer::class, [ 'calculateCacheKey', + 'getRequest', 'getTypoScriptFrontendController', ] ); @@ -2717,13 +2719,18 @@ final class ContentObjectRendererTest extends UnitTestCase ->method('calculateCacheKey') ->with($conf) ->willReturn($cacheKey); + $request = (new ServerRequest())->withAttribute('frontend.cache.instruction', new CacheInstruction()); + $subject + ->expects(self::once()) + ->method('getRequest') + ->willReturn($request); $typoScriptFrontendController = $this->createMock(TypoScriptFrontendController::class); $typoScriptFrontendController ->expects(self::exactly($times)) ->method('addCacheTags') ->with($tags); $subject - ->expects(self::exactly($times + 1)) + ->expects(self::exactly($times)) ->method('getTypoScriptFrontendController') ->willReturn($typoScriptFrontendController); $cacheFrontend = $this->createMock(CacheFrontendInterface::class); diff --git a/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php b/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php index 62a473275b0762b66cef235a68931089d60023cd..7810e27eb5dace865f0b9e9d340d362589a1df18 100644 --- a/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Middleware/PageArgumentValidatorTest.php @@ -25,29 +25,20 @@ use TYPO3\CMS\Core\Http\Response; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Information\Typo3Information; use TYPO3\CMS\Core\Routing\PageArguments; -use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Middleware\PageArgumentValidator; -use TYPO3\CMS\Frontend\Middleware\PageResolver; use TYPO3\CMS\Frontend\Page\CacheHashCalculator; -use TYPO3\TestingFramework\Core\AccessibleObjectInterface; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; final class PageArgumentValidatorTest extends UnitTestCase { protected bool $resetSingletonInstances = true; - protected CacheHashCalculator $cacheHashCalculator; - protected TimeTracker $timeTrackerStub; - protected RequestHandlerInterface $responseOutputHandler; - protected PageResolver&AccessibleObjectInterface $subject; + private RequestHandlerInterface $responseOutputHandler; protected function setUp(): void { parent::setUp(); - $this->timeTrackerStub = new TimeTracker(false); - $this->cacheHashCalculator = new CacheHashCalculator(); - // A request handler which only runs through $this->responseOutputHandler = new class () implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface @@ -70,7 +61,7 @@ final class PageArgumentValidatorTest extends UnitTestCase $request = new ServerRequest($incomingUrl, 'GET'); $request = $request->withAttribute('routing', $pageArguments); - $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub); + $subject = new PageArgumentValidator(new CacheHashCalculator()); $subject->setLogger(new NullLogger()); $response = $subject->process($request, $this->responseOutputHandler); @@ -90,7 +81,7 @@ final class PageArgumentValidatorTest extends UnitTestCase $request = new ServerRequest($incomingUrl, 'GET'); $request = $request->withAttribute('routing', $pageArguments); - $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub); + $subject = new PageArgumentValidator(new CacheHashCalculator()); $typo3InformationMock = $this->getMockBuilder(Typo3Information::class)->disableOriginalConstructor()->getMock(); $typo3InformationMock->expects(self::once())->method('getCopyrightYear')->willReturn('1999-20XX'); GeneralUtility::addInstance(Typo3Information::class, $typo3InformationMock); @@ -107,7 +98,7 @@ final class PageArgumentValidatorTest extends UnitTestCase $incomingUrl = 'https://king.com/lotus-flower/en/mr-magpie/bloom/'; $request = new ServerRequest($incomingUrl, 'GET'); - $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub); + $subject = new PageArgumentValidator(new CacheHashCalculator()); $typo3InformationMock = $this->getMockBuilder(Typo3Information::class)->disableOriginalConstructor()->getMock(); $typo3InformationMock->expects(self::once())->method('getCopyrightYear')->willReturn('1999-20XX'); GeneralUtility::addInstance(Typo3Information::class, $typo3InformationMock); @@ -127,7 +118,7 @@ final class PageArgumentValidatorTest extends UnitTestCase $request = new ServerRequest($incomingUrl, 'GET'); $request = $request->withAttribute('routing', $pageArguments); - $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub); + $subject = new PageArgumentValidator(new CacheHashCalculator()); $response = $subject->process($request, $this->responseOutputHandler); self::assertEquals(200, $response->getStatusCode()); } @@ -144,7 +135,7 @@ final class PageArgumentValidatorTest extends UnitTestCase $request = new ServerRequest($incomingUrl, 'GET'); $request = $request->withAttribute('routing', $pageArguments); - $subject = new PageArgumentValidator($this->cacheHashCalculator, $this->timeTrackerStub); + $subject = new PageArgumentValidator(new CacheHashCalculator()); $typo3InformationMock = $this->getMockBuilder(Typo3Information::class)->disableOriginalConstructor()->getMock(); $typo3InformationMock->expects(self::once())->method('getCopyrightYear')->willReturn('1999-20XX'); GeneralUtility::addInstance(Typo3Information::class, $typo3InformationMock); diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php index d0af47f1dc140a68670dbd99d059d8363f9afa75..1bedbb7197c59587f07d50b9ed94d1da68509b8d 100644 --- a/typo3/sysext/redirects/Classes/Service/RedirectService.php +++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php @@ -37,6 +37,7 @@ use TYPO3\CMS\Core\Site\Entity\SiteInterface; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder; use TYPO3\CMS\Frontend\Typolink\UnableToLinkException; @@ -386,6 +387,8 @@ class RedirectService implements LoggerAwareInterface */ protected function bootFrontendController(SiteInterface $site, array $queryParams, ServerRequestInterface $originalRequest): TypoScriptFrontendController { + $cacheInstruction = $originalRequest->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $originalRequest = $originalRequest->withAttribute('frontend.cache.instruction', $cacheInstruction); $controller = GeneralUtility::makeInstance( TypoScriptFrontendController::class, GeneralUtility::makeInstance(Context::class), diff --git a/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php b/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php index 47b1001705012947741ac55ee41891db5aad36bf..c1f922ceb69d7e0f2338432a9cf861d135055696 100644 --- a/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php +++ b/typo3/sysext/workspaces/Classes/Middleware/WorkspacePreview.php @@ -38,6 +38,7 @@ use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Localization\LanguageServiceFactory; use TYPO3\CMS\Core\Routing\RouteResultInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Cache\CacheInstruction; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\CMS\Workspaces\Authentication\PreviewUserAuthentication; @@ -74,7 +75,6 @@ class WorkspacePreview implements MiddlewareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $addInformationAboutDisabledCache = false; $keyword = $this->getPreviewInputCode($request); $setCookieOnCurrentRequest = false; $normalizedParams = $request->getAttribute('normalizedParams'); @@ -118,20 +118,16 @@ class WorkspacePreview implements MiddlewareInterface $GLOBALS['BE_USER']->setTemporaryWorkspace(0); // Register the backend user as aspect $this->setBackendUserAspect($context, $GLOBALS['BE_USER']); - // Caching is disabled, because otherwise generated URLs could include the keyword parameter - $request = $request->withAttribute('noCache', true); - $addInformationAboutDisabledCache = true; + $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); + $cacheInstruction->disableCache('ext:workspaces: Disabled FE cache with BE_USER previewing live workspace'); + $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); $setCookieOnCurrentRequest = false; } $response = $handler->handle($request); - $tsfe = $this->getTypoScriptFrontendController(); - if ($tsfe !== null && $addInformationAboutDisabledCache) { - $tsfe->set_no_cache('GET Parameter ADMCMD_prev=LIVE was given', true); - } - // Add an info box to the frontend content + $tsfe = $this->getTypoScriptFrontendController(); if ($tsfe !== null && $context->getPropertyFromAspect('workspace', 'isOffline', false)) { $previewInfo = $this->renderPreviewInfo($tsfe, $request->getUri()); $body = $response->getBody();