From b193d256ae1b71cebb3a8ea1dc0de75015e634e4 Mon Sep 17 00:00:00 2001 From: Helmut Hummel Date: Tue, 3 Jan 2023 18:02:01 +0100 Subject: [PATCH] [TASK] Provide current content object as request attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extbase ConfigurationManager as stateful singleton object is updated within extbase bootstrap for each plugin call. This is ugly, but since ConfigurationManager can be injected to other extbase services, which can be injected itself, it is very hard to get rid of. However, ConfigurationManager is also abused to "park" the current ContentObjectRenderer instance in this "convenient" singleton. The current content object renderer instance can then be retrieved at extbase runtime using getContentObject(). The form extension uses this at a couple of places to move the ContentObjectRenderer data around. The ConfigurationManager should however not be a source of such data, and our main "state" object in the framework is the request (which ConfigurationManager needs as well, but that's a different patch). To allow a deprecation of getContentObject() in ConfigurationManager, the patch changes the codebase to attach the current content object as request attribute instead. This is a bit fiddly as well since ContentObjectRenderer can be recursive (cObj rendering other cObj again), but the change at least makes this state more obvious, and we're pretty confident the situation will further relax and become more transparent with continued ContentObjectRenderer refactorings. Some of these have been prepared in v12 already and we'll see more on this with v13. The patch does the main refactoring, deprecating getContentObject() will follow with another patch. The patch does not add the attribute at all places where ContentObjectRenderer instances are created, more may follow later. The main goal of this patch is to create the main infrastructure and to not break the form extension. Resolves: #100623 Releases: main Change-Id: I3de7f53244c0b438ef54940d87a169068f1a832e Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77252 Tested-by: Christian Kuhn Reviewed-by: Christian Kuhn Tested-by: Benni Mack Tested-by: Stefan Bürk Reviewed-by: Stefan Bürk Reviewed-by: Benni Mack Tested-by: core-ci --- .../Configuration/ConfigurationManager.php | 1 + .../ConfigurationManagerInterface.php | 1 + .../ExtbasePluginContentObject.php | 4 +- .../sysext/extbase/Classes/Core/Bootstrap.php | 20 ++++++---- .../Classes/Mvc/Web/Routing/UriBuilder.php | 38 ++++++------------ .../Unit/Mvc/Web/Routing/UriBuilderTest.php | 1 - .../Mvc/Web/Routing/UriBuilderTest.php | 1 - .../Classes/ViewHelpers/CObjectViewHelper.php | 2 +- .../Controller/FormFrontendController.php | 4 +- .../Domain/Finishers/ConfirmationFinisher.php | 18 +++------ .../Classes/Domain/Runtime/FormRuntime.php | 6 +-- .../Controller/FormCachingTestsController.php | 4 +- .../Controller/FormFrontendControllerTest.php | 40 +++++++++---------- .../ContentObject/ContentObjectRenderer.php | 32 ++++++++++++--- .../TypoScriptFrontendController.php | 2 +- .../DataProcessing/FlexFormProcessor.php | 10 +++-- .../ContentObject/CaseContentObjectTest.php | 3 +- .../DataProcessing/FlexFormProcessorTest.php | 4 ++ 18 files changed, 101 insertions(+), 90 deletions(-) diff --git a/typo3/sysext/extbase/Classes/Configuration/ConfigurationManager.php b/typo3/sysext/extbase/Classes/Configuration/ConfigurationManager.php index 4c25c4eaee71..5294c210258f 100644 --- a/typo3/sysext/extbase/Classes/Configuration/ConfigurationManager.php +++ b/typo3/sysext/extbase/Classes/Configuration/ConfigurationManager.php @@ -67,6 +67,7 @@ public function getContentObject(): ?ContentObjectRenderer * Note that this is a low level method and only makes sense to be used by Extbase internally. * * @param array $configuration The new configuration + * @internal */ public function setConfiguration(array $configuration = []): void { diff --git a/typo3/sysext/extbase/Classes/Configuration/ConfigurationManagerInterface.php b/typo3/sysext/extbase/Classes/Configuration/ConfigurationManagerInterface.php index 9b1e79a24b00..be87d35b3c4f 100644 --- a/typo3/sysext/extbase/Classes/Configuration/ConfigurationManagerInterface.php +++ b/typo3/sysext/extbase/Classes/Configuration/ConfigurationManagerInterface.php @@ -55,6 +55,7 @@ public function getConfiguration(string $configurationType, ?string $extensionNa * Note that this is a low level method and only makes sense to be used by Extbase internally. * * @param array $configuration The new configuration + * @internal */ public function setConfiguration(array $configuration = []): void; diff --git a/typo3/sysext/extbase/Classes/ContentObject/ExtbasePluginContentObject.php b/typo3/sysext/extbase/Classes/ContentObject/ExtbasePluginContentObject.php index 121866f7743d..088f3291faee 100644 --- a/typo3/sysext/extbase/Classes/ContentObject/ExtbasePluginContentObject.php +++ b/typo3/sysext/extbase/Classes/ContentObject/ExtbasePluginContentObject.php @@ -41,8 +41,8 @@ public function render($conf = []) // Come here only if we are not called from $TSFE->processNonCacheableContentPartsAndSubstituteContentMarkers()! $this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER); } - $extbaseBootstrap->initialize($conf); - $content = $extbaseBootstrap->handleFrontendRequest($this->request); + $request = $extbaseBootstrap->initialize($conf, $this->request); + $content = $extbaseBootstrap->handleFrontendRequest($request); // Rendering is deferred, as the action should not be cached, we pump this now to TSFE to be executed later-on if ($this->cObj->doConvertToUserIntObject) { $this->cObj->doConvertToUserIntObject = false; diff --git a/typo3/sysext/extbase/Classes/Core/Bootstrap.php b/typo3/sysext/extbase/Classes/Core/Bootstrap.php index 78d97d2a7e3b..aaca576c3aa0 100644 --- a/typo3/sysext/extbase/Classes/Core/Bootstrap.php +++ b/typo3/sysext/extbase/Classes/Core/Bootstrap.php @@ -79,14 +79,14 @@ public function setContentObjectRenderer(ContentObjectRenderer $cObj) /** * Explicitly initializes all necessary Extbase objects by invoking the various initialize* methods. * - * Usually this method is only called from unit tests or other applications which need a more fine grained control over + * Usually this method is only called from unit tests or other applications which need a more fine-grained control over * the initialization and request handling process. Most other applications just call the run() method. * * @param array $configuration The TS configuration array * @throws \RuntimeException * @see run() */ - public function initialize(array $configuration): void + public function initialize(array $configuration, ServerRequestInterface $request): ServerRequestInterface { if (!Environment::isCli()) { if (!isset($configuration['extensionName']) || $configuration['extensionName'] === '') { @@ -96,7 +96,7 @@ public function initialize(array $configuration): void throw new \RuntimeException('Invalid configuration: "pluginName" is not set', 1290623027); } } - $this->initializeConfiguration($configuration); + return $this->initializeConfiguration($configuration, $request); } /** @@ -105,11 +105,16 @@ public function initialize(array $configuration): void * @see initialize() * @internal */ - public function initializeConfiguration(array $configuration): void + public function initializeConfiguration(array $configuration, ServerRequestInterface $request): ServerRequestInterface { - $this->cObj ??= $this->container->get(ContentObjectRenderer::class); + if ($this->cObj === null) { + $this->cObj = $this->container->get(ContentObjectRenderer::class); + $request = $request->withAttribute('currentContentObject', $this->cObj); + } + $this->cObj->setRequest($request); $this->configurationManager->setContentObject($this->cObj); $this->configurationManager->setConfiguration($configuration); + return $request; // todo: Shouldn't the configuration manager object – which is a singleton – be stateless? // todo: At this point we give the configuration manager a state, while we could directly pass the // todo: configuration (i.e. controllerName, actionName and such), directly to the request @@ -133,7 +138,7 @@ public function initializeConfiguration(array $configuration): void */ public function run(string $content, array $configuration, ServerRequestInterface $request): string { - $this->initialize($configuration); + $request = $this->initialize($configuration, $request); return $this->handleFrontendRequest($request); } @@ -204,7 +209,6 @@ public function handleFrontendRequest(ServerRequestInterface $request): string * * Creates an Extbase Request, dispatches it and then returns the Response * - * @param ServerRequestInterface $request * @internal */ public function handleBackendRequest(ServerRequestInterface $request): ResponseInterface @@ -216,7 +220,7 @@ public function handleBackendRequest(ServerRequestInterface $request): ResponseI 'pluginName' => $module?->getIdentifier(), ]; - $this->initialize($configuration); + $request = $this->initialize($configuration, $request); $extbaseRequest = $this->extbaseRequestBuilder->build($request); $response = $this->dispatcher->dispatch($extbaseRequest); $this->resetSingletons(); diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php index 29f72e9974c9..b21216b825b9 100644 --- a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php +++ b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php @@ -40,22 +40,11 @@ */ class UriBuilder { - /** - * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface - */ - protected $configurationManager; + protected ConfigurationManagerInterface $configurationManager; - /** - * @var \TYPO3\CMS\Extbase\Service\ExtensionService - */ - protected $extensionService; + protected ExtensionService $extensionService; - /** - * An instance of \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer - * - * @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer - */ - protected $contentObject; + protected ContentObjectRenderer $contentObject; protected ?RequestInterface $request = null; @@ -147,16 +136,6 @@ public function injectExtensionService(ExtensionService $extensionService): void $this->extensionService = $extensionService; } - /** - * Life-cycle method that is called by the DI container as soon as this object is completely built - * @internal only to be used within Extbase, not part of TYPO3 Core API. - */ - public function initializeObject(): void - { - $this->contentObject = $this->configurationManager->getContentObject() - ?? GeneralUtility::makeInstance(ContentObjectRenderer::class); - } - /** * Sets the current request * @@ -165,6 +144,15 @@ public function initializeObject(): void public function setRequest(RequestInterface $request): UriBuilder { $this->request = $request; + $contentObject = $request->getAttribute('currentContentObject'); + if ($contentObject === null) { + // @todo: Review this. This should never be the case since extbase + // bootstrap adds 'currentContentObject' to request. When this + // if() kicks in, it most likely indicates an "out of scope" usage. + $contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $contentObject->setRequest($request->withAttribute('currentContentObject', $contentObject)); + } + $this->contentObject = $contentObject; return $this; } @@ -478,7 +466,7 @@ public function reset(): UriBuilder /* * $this->request MUST NOT be reset here because the request is actually a hard dependency and not part * of the internal state of this object. - * todo: consider making the request a constructor dependency or get rid of it's usage + * todo: make the request a constructor dependency */ return $this; } diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php index a1743d08c9aa..e4ad44a0e1f0 100644 --- a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php @@ -73,7 +73,6 @@ protected function setUp(): void $this->subject->setRequest($this->mockRequest); $this->subject->injectConfigurationManager($this->createMock(ConfigurationManagerInterface::class)); $this->subject->injectExtensionService($this->mockExtensionService); - $this->subject->initializeObject(); $this->subject->_set('contentObject', $this->mockContentObject); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); $router = new Router($requestContextFactory); diff --git a/typo3/sysext/extbase/Tests/UnitDeprecated/Mvc/Web/Routing/UriBuilderTest.php b/typo3/sysext/extbase/Tests/UnitDeprecated/Mvc/Web/Routing/UriBuilderTest.php index 9492914d0012..d50addfc1993 100644 --- a/typo3/sysext/extbase/Tests/UnitDeprecated/Mvc/Web/Routing/UriBuilderTest.php +++ b/typo3/sysext/extbase/Tests/UnitDeprecated/Mvc/Web/Routing/UriBuilderTest.php @@ -41,7 +41,6 @@ protected function setUp(): void $this->subject->setRequest($this->createMock(Request::class)); $this->subject->injectConfigurationManager($this->createMock(ConfigurationManagerInterface::class)); $this->subject->injectExtensionService($this->mockExtensionService); - $this->subject->initializeObject(); $this->subject->_set('contentObject', $this->createMock(ContentObjectRenderer::class)); } diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/CObjectViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/CObjectViewHelper.php index e814d8f25b90..4e3e2bb4a7eb 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/CObjectViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/CObjectViewHelper.php @@ -128,7 +128,7 @@ public static function renderStatic(array $arguments, \Closure $renderChildrenCl /** @var RenderingContext $renderingContext */ $request = $renderingContext->getRequest(); $contentObjectRenderer = self::getContentObjectRenderer($request); - $contentObjectRenderer->setRequest($request); + $contentObjectRenderer->setRequest($request->withAttribute('currentContentObject', $contentObjectRenderer)); $tsfeBackup = null; if (!isset($GLOBALS['TSFE']) || !($GLOBALS['TSFE'] instanceof TypoScriptFrontendController)) { $tsfeBackup = self::simulateFrontendEnvironment(); diff --git a/typo3/sysext/form/Classes/Controller/FormFrontendController.php b/typo3/sysext/form/Classes/Controller/FormFrontendController.php index 42f56fcddf6c..a331ad2f4862 100644 --- a/typo3/sysext/form/Classes/Controller/FormFrontendController.php +++ b/typo3/sysext/form/Classes/Controller/FormFrontendController.php @@ -69,7 +69,7 @@ public function renderAction(): ResponseInterface $formDefinition['persistenceIdentifier'] = $this->settings['persistenceIdentifier']; $formDefinition = $this->overrideByFlexFormSettings($formDefinition); $formDefinition = ArrayUtility::setValueByPath($formDefinition, 'renderingOptions._originalIdentifier', $formDefinition['identifier'], '.'); - $formDefinition['identifier'] .= '-' . $this->configurationManager->getContentObject()->data['uid']; + $formDefinition['identifier'] .= '-' . $this->request->getAttribute('currentContentObject')->data['uid']; } $this->view->assign('formConfiguration', $formDefinition); @@ -93,7 +93,7 @@ public function performAction(): ResponseInterface */ protected function overrideByFlexFormSettings(array $formDefinition): array { - $flexFormData = GeneralUtility::xml2array($this->configurationManager->getContentObject()->data['pi_flexform'] ?? ''); + $flexFormData = GeneralUtility::xml2array($this->request->getAttribute('currentContentObject')->data['pi_flexform'] ?? ''); if (!is_array($flexFormData)) { return $formDefinition; diff --git a/typo3/sysext/form/Classes/Domain/Finishers/ConfirmationFinisher.php b/typo3/sysext/form/Classes/Domain/Finishers/ConfirmationFinisher.php index 4731641a98ad..9092fb5f36b2 100644 --- a/typo3/sysext/form/Classes/Domain/Finishers/ConfirmationFinisher.php +++ b/typo3/sysext/form/Classes/Domain/Finishers/ConfirmationFinisher.php @@ -57,20 +57,11 @@ class ConfirmationFinisher extends AbstractFinisher 'typoscriptObjectPath' => 'lib.tx_form.contentElementRendering', ]; - /** - * @var array - */ - protected $typoScriptSetup = []; + protected array $typoScriptSetup = []; - /** - * @var ConfigurationManagerInterface - */ - protected $configurationManager; + protected ConfigurationManagerInterface $configurationManager; - /** - * @var ContentObjectRenderer - */ - protected $contentObjectRenderer; + protected ContentObjectRenderer $contentObjectRenderer; public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager) { @@ -109,7 +100,8 @@ protected function executeInternal() } $setup = $setup[$segment . '.']; } - $this->contentObjectRenderer->start([$contentElementUid], ''); + $this->contentObjectRenderer->setRequest($this->finisherContext->getRequest()); + $this->contentObjectRenderer->start([$contentElementUid]); $this->contentObjectRenderer->setCurrentVal((string)$contentElementUid); $message = $this->contentObjectRenderer->cObjGetSingle($setup[$lastSegment], $setup[$lastSegment . '.'], $lastSegment); } else { diff --git a/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php index 77c3508b59c3..4d0bf895210f 100644 --- a/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php +++ b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php @@ -504,7 +504,7 @@ protected function isPostRequest(): bool */ protected function isRenderedCached(): bool { - $contentObject = $this->configurationManager->getContentObject(); + $contentObject = $this->request->getAttribute('currentContentObject'); return $contentObject === null ? true // @todo this does not work when rendering a cached `FLUIDTEMPLATE` (not nested in `COA_INT`) @@ -1079,8 +1079,8 @@ protected function getConditionResolver(): Resolver } $contentObjectData = []; - if ($this->configurationManager->getContentObject() instanceof ContentObjectRenderer) { - $contentObjectData = $this->configurationManager->getContentObject()->data; + if ($this->request->getAttribute('currentContentObject') instanceof ContentObjectRenderer) { + $contentObjectData = $this->request->getAttribute('currentContentObject')->data; } return GeneralUtility::makeInstance( diff --git a/typo3/sysext/form/Tests/Functional/RequestHandling/Fixtures/Extensions/form_caching_tests/Classes/Controller/FormCachingTestsController.php b/typo3/sysext/form/Tests/Functional/RequestHandling/Fixtures/Extensions/form_caching_tests/Classes/Controller/FormCachingTestsController.php index a5f24ffae29a..f613d10fc4af 100644 --- a/typo3/sysext/form/Tests/Functional/RequestHandling/Fixtures/Extensions/form_caching_tests/Classes/Controller/FormCachingTestsController.php +++ b/typo3/sysext/form/Tests/Functional/RequestHandling/Fixtures/Extensions/form_caching_tests/Classes/Controller/FormCachingTestsController.php @@ -24,14 +24,14 @@ class FormCachingTestsController extends ActionController { public function someRenderAction(): ResponseInterface { - $this->view->assign('formIdentifier', $this->request->getPluginName() . '-' . $this->configurationManager->getContentObject()->data['uid']); + $this->view->assign('formIdentifier', $this->request->getPluginName() . '-' . $this->request->getAttribute('currentContentObject')->data['uid']); $this->view->assign('pluginName', $this->request->getPluginName()); return $this->htmlResponse(); } public function somePerformAction(): ResponseInterface { - $this->view->assign('formIdentifier', $this->request->getPluginName() . '-' . $this->configurationManager->getContentObject()->data['uid']); + $this->view->assign('formIdentifier', $this->request->getPluginName() . '-' . $this->request->getAttribute('currentContentObject')->data['uid']); $this->view->assign('pluginName', $this->request->getPluginName()); return $this->htmlResponse(); } diff --git a/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php b/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php index bc6f8837f6c0..939b2372ba35 100644 --- a/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php +++ b/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php @@ -21,9 +21,11 @@ use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\NullFrontend; use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Tests\Unit\Fixtures\EventDispatcher\MockEventDispatcher; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Configuration\FrontendConfigurationManager; +use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; +use TYPO3\CMS\Extbase\Mvc\Request; use TYPO3\CMS\Form\Controller\FormFrontendController; use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -65,7 +67,12 @@ public function overrideByFlexFormSettingsReturnsNoOverriddenConfigurationIfFlex ); $flexFormTools = new FlexFormTools(new MockEventDispatcher()); + $request = (new ServerRequest())->withAttribute('extbase', new ExtbaseRequestParameters()); + $request = (new Request($request)); $contentObject = new ContentObjectRenderer(); + $request = $request->withAttribute('currentContentObject', $contentObject); + $contentObject->setRequest($request); + $mockController->_set('request', $request); $contentObject->data = [ 'pi_flexform' => $flexFormTools->flexArray2Xml([ 'data' => [ @@ -99,13 +106,6 @@ public function overrideByFlexFormSettingsReturnsNoOverriddenConfigurationIfFlex ]), ]; - $frontendConfigurationManager = $this->createMock(FrontendConfigurationManager::class); - $frontendConfigurationManager - ->method('getContentObject') - ->willReturn($contentObject); - - $mockController->_set('configurationManager', $frontendConfigurationManager); - $configurationServiceMock->method('getPrototypeConfiguration')->with(self::anything())->willReturn([ 'finishersDefinition' => [ 'EmailToReceiver' => [ @@ -196,7 +196,12 @@ public function overrideByFlexFormSettingsReturnsOverriddenConfigurationIfFlexfo ); $flexFormTools = new FlexFormTools(new MockEventDispatcher()); + $request = (new ServerRequest())->withAttribute('extbase', new ExtbaseRequestParameters()); + $request = (new Request($request)); $contentObject = new ContentObjectRenderer(); + $request = $request->withAttribute('currentContentObject', $contentObject); + $contentObject->setRequest($request); + $mockController->_set('request', $request); $contentObject->data = [ 'pi_flexform' => $flexFormTools->flexArray2Xml([ 'data' => [ @@ -230,13 +235,6 @@ public function overrideByFlexFormSettingsReturnsOverriddenConfigurationIfFlexfo ]), ]; - $frontendConfigurationManager = $this->createMock(FrontendConfigurationManager::class); - $frontendConfigurationManager - ->method('getContentObject') - ->willReturn($contentObject); - - $mockController->_set('configurationManager', $frontendConfigurationManager); - $configurationServiceMock->method('getPrototypeConfiguration')->with(self::anything())->willReturn([ 'finishersDefinition' => [ 'EmailToReceiver' => [ @@ -354,7 +352,12 @@ public function overrideByFlexFormSettingsReturnsNotOverriddenConfigurationKeyIf GeneralUtility::addInstance(FlexFormTools::class, new FlexFormTools($eventDispatcher)); $flexFormTools = new FlexFormTools(new MockEventDispatcher()); + $request = (new ServerRequest())->withAttribute('extbase', new ExtbaseRequestParameters()); + $request = (new Request($request)); $contentObject = new ContentObjectRenderer(); + $request = $request->withAttribute('currentContentObject', $contentObject); + $contentObject->setRequest($request); + $mockController->_set('request', $request); $contentObject->data = [ 'pi_flexform' => $flexFormTools->flexArray2Xml([ 'data' => [ @@ -388,13 +391,6 @@ public function overrideByFlexFormSettingsReturnsNotOverriddenConfigurationKeyIf ]), ]; - $frontendConfigurationManager = $this->createMock(FrontendConfigurationManager::class); - $frontendConfigurationManager - ->method('getContentObject') - ->willReturn($contentObject); - - $mockController->_set('configurationManager', $frontendConfigurationManager); - $configurationServiceMock->method('getPrototypeConfiguration')->with(self::anything())->willReturn([ 'finishersDefinition' => [ 'EmailToReceiver' => [ diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php index 76126deefcb7..bb361a8a44db 100644 --- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php +++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php @@ -468,8 +468,7 @@ public function __wakeup() $this->container = GeneralUtility::getContainer(); // We do not derive $this->request from globals here. The request is expected to be injected - // using setRequest() after deserialization or with start(). - // (A fallback to $GLOBALS['TYPO3_REQUEST'] is available in getRequest() for BC) + // using setRequest(), a fallback to $GLOBALS['TYPO3_REQUEST'] is available in getRequest() for BC. } /** @@ -5847,16 +5846,39 @@ protected function shallDebug(): bool return !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']); } + /** + * @todo: This getRequest() handling is pretty messy. We created a loop from + * request to 'currentContentObject' back to request with this. + * v13 should be refactored, probably with this patch chain: + * * Remove fallback to $GLOBALS['TYPO3_REQUEST'] to force consumers + * actually setting the request using setRequest(). + * * Protect this method. + * * Get rid of public TSFE->cObj (the "page" instance of cObj). + * * Avoid TSFE as constructor argument and make ContentObjectRenderer + * free for DI, for instance to get the container injected. + * * When getRequest() is protected or private, setRequest() should + * *remove* the currentContentObject attribute again, to prevent + * the object loop. This will work, since local getRequest() could + * use $this when needed. + * + * @internal This method will be set to protected with TYPO3 v13. + */ public function getRequest(): ServerRequestInterface { if ($this->request instanceof ServerRequestInterface) { + // Note attribute 'currentContentObject' has been set by setRequest() already. return $this->request; } - if (isset($GLOBALS['TYPO3_REQUEST']) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { - return $GLOBALS['TYPO3_REQUEST']; + if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) { + // @todo: We may want to deprecate this fallback and force consumers + // to setRequest() after object instantiation / unserialization instead. + return $GLOBALS['TYPO3_REQUEST']->withAttribute('currentContentObject', $this); } - throw new ContentRenderingException('PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', 1607172972); + throw new ContentRenderingException( + 'PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', + 1607172972 + ); } } diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index 8dc8c92f6dcc..e009ba0bc96e 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -2276,7 +2276,7 @@ protected function processNonCacheableContentPartsAndSubstituteContentMarkers(ar $nonCacheableContent = ''; $contentObjectRendererForNonCacheable = unserialize($nonCacheableData[$nonCacheableKey]['cObj']); /* @var ContentObjectRenderer $contentObjectRendererForNonCacheable */ - $contentObjectRendererForNonCacheable->setRequest($request); + $contentObjectRendererForNonCacheable->setRequest($request->withAttribute('currentContentObject', $contentObjectRendererForNonCacheable)); switch ($nonCacheableData[$nonCacheableKey]['type']) { case 'COA': $nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('COA', $nonCacheableData[$nonCacheableKey]['conf']); diff --git a/typo3/sysext/frontend/Classes/DataProcessing/FlexFormProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/FlexFormProcessor.php index 830019b1f047..f9082c60373d 100644 --- a/typo3/sysext/frontend/Classes/DataProcessing/FlexFormProcessor.php +++ b/typo3/sysext/frontend/Classes/DataProcessing/FlexFormProcessor.php @@ -17,6 +17,7 @@ namespace TYPO3\CMS\Frontend\DataProcessing; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor; @@ -79,7 +80,9 @@ public function process( $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'flexFormData'); if (isset($processorConfiguration['dataProcessing.']) && is_array($processorConfiguration['dataProcessing.'])) { - $flexFormData = $this->processAdditionalDataProcessors($flexFormData, $processorConfiguration); + // @todo: It looks as if data processors should retrieve the current request from the outside, + // this would avoid $cObj->getRequest() here. + $flexFormData = $this->processAdditionalDataProcessors($flexFormData, $processorConfiguration, $cObj->getRequest()); } $processedData[$targetVariableName] = $flexFormData; @@ -90,10 +93,11 @@ public function process( /** * Recursively process sub processors of a data processor */ - public function processAdditionalDataProcessors(array $data, array $processorConfiguration): array + protected function processAdditionalDataProcessors(array $data, array $processorConfiguration, ServerRequestInterface $request): array { $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); - $contentObjectRenderer->start([$data]); + $contentObjectRenderer->setRequest($request->withAttribute('currentContentObject', $contentObjectRenderer)); + $contentObjectRenderer->start([$data], ''); return GeneralUtility::makeInstance(ContentDataProcessor::class)->process( $contentObjectRenderer, $processorConfiguration, diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/CaseContentObjectTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/CaseContentObjectTest.php index 7041a175a936..95b64783ef13 100644 --- a/typo3/sysext/frontend/Tests/Unit/ContentObject/CaseContentObjectTest.php +++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/CaseContentObjectTest.php @@ -43,6 +43,7 @@ protected function setUp(): void $request = new ServerRequest(); $contentObjectRenderer = new ContentObjectRenderer($tsfe); + $request = $request->withAttribute('currentContentObject', $contentObjectRenderer); $contentObjectRenderer->setRequest($request); $cObjectFactoryMock = $this->getMockBuilder(ContentObjectFactory::class)->disableOriginalConstructor()->getMock(); @@ -51,7 +52,7 @@ protected function setUp(): void $caseContentObject->setContentObjectRenderer($contentObjectRenderer); $textContentObject = new TextContentObject(); - $textContentObject->setRequest(new ServerRequest()); + $textContentObject->setRequest($request); $textContentObject->setContentObjectRenderer($contentObjectRenderer); $cObjectFactoryMock->method('getContentObject')->willReturnMap([ diff --git a/typo3/sysext/frontend/Tests/Unit/DataProcessing/FlexFormProcessorTest.php b/typo3/sysext/frontend/Tests/Unit/DataProcessing/FlexFormProcessorTest.php index 0c1f75771bb0..6763db6861a6 100644 --- a/typo3/sysext/frontend/Tests/Unit/DataProcessing/FlexFormProcessorTest.php +++ b/typo3/sysext/frontend/Tests/Unit/DataProcessing/FlexFormProcessorTest.php @@ -18,6 +18,7 @@ namespace TYPO3\CMS\Frontend\Tests\Unit\DataProcessing; use PHPUnit\Framework\MockObject\MockObject; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor; @@ -204,6 +205,9 @@ public function subDataProcessorIsResolved(): void ], ]; $this->contentObjectRendererMock->expects(self::once())->method('start')->with([$convertedFlexFormData]); + $request = new ServerRequest(); + $request = $request->withAttribute('currentContentObject', $this->contentObjectRendererMock); + $this->contentObjectRendererMock->method('getRequest')->willReturn($request); $contentDataProcessorMock = $this->getMockBuilder(ContentDataProcessor::class)->disableOriginalConstructor()->getMock(); $renderedDataFromProcessors = [