diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-78002-EnforceCHashArgumentForExtbaseActions.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-78002-EnforceCHashArgumentForExtbaseActions.rst new file mode 100644 index 0000000000000000000000000000000000000000..5fa94ae25c31939404dab84baa09a00774d76996 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-78002-EnforceCHashArgumentForExtbaseActions.rst @@ -0,0 +1,40 @@ +.. include:: ../../Includes.txt + +============================================================= +Breaking: #78002 - Enforce cHash argument for Extbase actions +============================================================= + +See :issue:`78002` + +Description +=========== + +URIs to Extbase actions now need a valid cHash per default. This is required for +both cached and uncached actions. The behavior can be disabled for all actions +using the feature switch ``requireCHashArgumentForActionArguments``. + + +Impact +====== + +All generated links to Extbase actions without having a valid cHash will fail. + + +Affected Installations +====================== + +All generated links to Extbase actions that explicitly disabled the cHash are +affected - like ``<f:link.action action="..." noCacheHash="1"/>`` + + +Migration +========= + +Either one of the following: + ++ ensure to use a valid cHash, e.g. by removing the + ``noCacheHash="1"`` argument from link view-helpers ++ disable the ``feature.requireCHashArgumentForActionArguments`` + setting for the particular extension + +.. index:: Frontend, PHP-API \ No newline at end of file diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-78002-EnforceCHashArgumentForExtbaseActions.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-78002-EnforceCHashArgumentForExtbaseActions.rst new file mode 100644 index 0000000000000000000000000000000000000000..a9c63529ca3904e85caa855b854abc35f17c5c4f --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-78002-EnforceCHashArgumentForExtbaseActions.rst @@ -0,0 +1,29 @@ +.. include:: ../../Includes.txt + +============================================================ +Feature: #78002 - Enforce cHash argument for Extbase actions +============================================================ + +See :issue:`78002` + +Description +=========== + +TypoScriptFrontendController::reqCHash() is now called for Extbase frontend +plugin actions just like they were usually called for the legacy AbstractPlugin. +This provides a more reliable page caching behavior by default and with zero +configuration for extension authors. + +With the feature switch ``requireCHashArgumentForActionArguments`` this behavior +can be disabled, which could be useful, if all actions in a plugin are uncached +or one wants to manually control the cHash behavior. + + +Impact +====== + +The enforcing of a cHash results in a 404, if plugin arguments are present, but +cHash is not, which would also happen if the plugin arguments were added to +``cHashRequiredParameters`` configuration. + +.. index:: Frontend, PHP-API \ No newline at end of file diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/CacheHashEnforcer.php b/typo3/sysext/extbase/Classes/Mvc/Web/CacheHashEnforcer.php new file mode 100644 index 0000000000000000000000000000000000000000..dba0a5db05adee32d97ed801cd2f0ee783c3005c --- /dev/null +++ b/typo3/sysext/extbase/Classes/Mvc/Web/CacheHashEnforcer.php @@ -0,0 +1,73 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Extbase\Mvc\Web; + +/* + * 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\SingletonInterface; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\Page\CacheHashCalculator; + +/** + * Enforces cHash argument if it is required for a given request + */ +class CacheHashEnforcer implements SingletonInterface +{ + /** + * @var CacheHashCalculator + */ + protected $cacheHashCalculator; + + /** + * @var TypoScriptFrontendController + */ + protected $typoScriptFrontendController; + + /** + * CacheHashEnforcer constructor. + * + * @param CacheHashCalculator $cacheHashCalculator + * @param TypoScriptFrontendController|null $typoScriptFrontendController + */ + public function __construct( + CacheHashCalculator $cacheHashCalculator, + TypoScriptFrontendController $typoScriptFrontendController = null + ) { + $this->cacheHashCalculator = $cacheHashCalculator; + $this->typoScriptFrontendController = $typoScriptFrontendController ?: $GLOBALS['TSFE']; + } + + /** + * Checks if cHash is required for the current request and calls + * TypoScriptFrontendController::reqCHash() if so. + * This call will trigger a PageNotFoundException if arguments are required and cHash is not present. + * + * @param Request $request + * @param string $pluginNamespace + */ + public function enforceForRequest(Request $request, string $pluginNamespace) + { + $arguments = $request->getArguments(); + if (is_array($arguments) && count($arguments) > 0) { + $parameters = [$pluginNamespace => $arguments]; + $parameters['id'] = $this->typoScriptFrontendController->id; + $relevantParameters = $this->cacheHashCalculator->getRelevantParameters( + http_build_query($parameters) + ); + if (count($relevantParameters) > 0) { + $this->typoScriptFrontendController->reqCHash(); + } + } + } +} diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/FrontendRequestHandler.php b/typo3/sysext/extbase/Classes/Mvc/Web/FrontendRequestHandler.php index 2c831f89cb81a2b5ae0f4acf8f65a1e684b51215..fa156ef75505284904a63985f470019dbfcf6421 100644 --- a/typo3/sysext/extbase/Classes/Mvc/Web/FrontendRequestHandler.php +++ b/typo3/sysext/extbase/Classes/Mvc/Web/FrontendRequestHandler.php @@ -29,6 +29,11 @@ class FrontendRequestHandler extends AbstractRequestHandler */ protected $extensionService; + /** + * @var \TYPO3\CMS\Extbase\Mvc\Web\CacheHashEnforcer + */ + protected $cacheHashEnforcer; + /** * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager */ @@ -45,6 +50,14 @@ class FrontendRequestHandler extends AbstractRequestHandler $this->extensionService = $extensionService; } + /** + * @param \TYPO3\CMS\Extbase\Mvc\Web\CacheHashEnforcer $cacheHashEnforcer + */ + public function injectCacheHashEnforcer(\TYPO3\CMS\Extbase\Mvc\Web\CacheHashEnforcer $cacheHashEnforcer) + { + $this->cacheHashEnforcer = $cacheHashEnforcer; + } + /** * Handles the web request. The response will automatically be sent to the client. * @@ -64,6 +77,15 @@ class FrontendRequestHandler extends AbstractRequestHandler } $request->setIsCached(false); } + + if ($this->configurationManager->isFeatureEnabled('requireCHashArgumentForActionArguments')) { + $pluginNamespace = $this->extensionService->getPluginNamespace( + $request->getControllerExtensionName(), + $request->getPluginName() + ); + $this->cacheHashEnforcer->enforceForRequest($request, $pluginNamespace); + } + /** @var $response \TYPO3\CMS\Extbase\Mvc\ResponseInterface */ $response = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\Web\Response::class); $this->dispatcher->dispatch($request, $response); diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/CacheHashEnforcerTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/CacheHashEnforcerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..43bf953f3a2f292e8f48f94012a340c7332a4b88 --- /dev/null +++ b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/CacheHashEnforcerTest.php @@ -0,0 +1,75 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Extbase\Tests\Unit\Mvc\Web; + +/* + * 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\Extbase\Mvc\Web\CacheHashEnforcer; +use TYPO3\CMS\Extbase\Mvc\Web\Request; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\Page\CacheHashCalculator; + +/** + * Test case + */ +class CacheHashEnforcerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase +{ + /** + * @var \TYPO3\CMS\Extbase\Mvc\Web\CacheHashEnforcer + */ + protected $subject; + + /** + * @var TypoScriptFrontendController|\PHPUnit_Framework_MockObject_MockObject + */ + protected $frontendControllerMock; + + protected function setUp() + { + $this->frontendControllerMock = $this->getMockBuilder(TypoScriptFrontendController::class)->disableOriginalConstructor()->getMock(); + $this->frontendControllerMock->id = 42; + $cacheHashCalculator = new CacheHashCalculator(); + $this->subject = new CacheHashEnforcer( + $cacheHashCalculator, + $this->frontendControllerMock + ); + } + + /** + * @test + */ + public function validateCallsReqCHashIfRequestArgumentsArePresent() + { + $request = new Request(); + $request->setArguments(['foo' => 'bar']); + $this->frontendControllerMock + ->expects($this->once()) + ->method('reqCHash'); + + $this->subject->enforceForRequest($request, 'tx_foo'); + } + + /** + * @test + */ + public function validateDoesNotCallsReqCHashIfNoRequestArgumentsArePresent() + { + $request = new Request(); + $this->frontendControllerMock + ->expects($this->never()) + ->method('reqCHash'); + + $this->subject->enforceForRequest($request, 'tx_foo'); + } +} diff --git a/typo3/sysext/extbase/ext_typoscript_setup.txt b/typo3/sysext/extbase/ext_typoscript_setup.txt index 125ac94e663398cb955af9fa718111ef0cc9d5c5..ffaa90767493d4399b36f952f17404f6ea670174 100644 --- a/typo3/sysext/extbase/ext_typoscript_setup.txt +++ b/typo3/sysext/extbase/ext_typoscript_setup.txt @@ -96,5 +96,7 @@ config.tx_extbase { skipDefaultArguments = 0 # if set to 1, the enable fields are ignored in BE context ignoreAllEnableFieldsInBe = 0 + # Should be on by default, but can be disabled if all action in the plugin are uncached + requireCHashArgumentForActionArguments = 1 } }