callUserFunction(). */ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void { $this->cObj = $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 * 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, ServerRequestInterface $request): ServerRequestInterface { if (!Environment::isCli()) { if (!isset($configuration['extensionName']) || $configuration['extensionName'] === '') { throw new \RuntimeException('Invalid configuration: "extensionName" is not set', 1290623020); } if (!isset($configuration['pluginName']) || $configuration['pluginName'] === '') { throw new \RuntimeException('Invalid configuration: "pluginName" is not set', 1290623027); } } return $this->initializeConfiguration($configuration, $request); } /** * Initializes the Object framework. * * @see initialize() * @internal */ public function initializeConfiguration(array $configuration, ServerRequestInterface $request): ServerRequestInterface { if ($this->cObj === null) { // @todo: While the frontend sets the current cObj, a backend extbase request does not. // It is currently not clear if the backend should have a dummy cObj as well. // For now, extbase initializes one. $this->cObj = $this->container->get(ContentObjectRenderer::class); $this->cObj->setRequest($request); } $this->configurationManager->setRequest($request); $this->configurationManager->setConfiguration($configuration); return $request; // todo: Outdated todo, recheck in v13. // Shouldn't the configuration manager object – which is a singleton – be stateless? // At this point we give the configuration manager a state, while we could directly pass the // configuration (i.e. controllerName, actionName and such), directly to the request // handler, which then creates stateful request objects. // Once this has changed, \TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder::loadDefaultValues does not need // to fetch this configuration from the configuration manager. } /** * Runs the Extbase Framework by resolving an appropriate Request Handler and passing control to it. * If the Framework is not initialized yet, it will be initialized. * * This is usually used in Frontend plugins. * This method will be marked as internal in the future, use EXTBASEPLUGIN in TypoScript to execute an Extbase plugin * instead. * * @param string $content The content. Not used * @param array $configuration The TS configuration array * @param ServerRequestInterface $request the incoming server request * @return string $content The processed content */ #[AsAllowedCallable] public function run(string $content, array $configuration, ServerRequestInterface $request): string { $request = $this->initialize($configuration, $request); return $this->handleFrontendRequest($request); } /** * Used for any Extbase Plugin in the Frontend, be sure to run $this->initialize() before. * * @internal */ public function handleFrontendRequest(ServerRequestInterface $request): string { $extbaseRequest = $this->extbaseRequestBuilder->build($request); if (!$this->isExtbaseRequestCacheable($extbaseRequest)) { if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) { // ContentObjectRenderer::convertToUserIntObject() will recreate the object, // so we have to stop the request here before the action is actually called $this->cObj->convertToUserIntObject(); return ''; } } // Dispatch the extbase request $response = $this->dispatcher->dispatch($extbaseRequest); if ($response->getStatusCode() >= 300) { // Avoid caching the plugin when we issue a redirect or error response // This means that even when an action is configured as cachable // we avoid the plugin to be cached, but keep the page cache untouched if ($this->cObj->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) { $this->cObj->convertToUserIntObject(); } } // Usually coming from an error action, ensure all caches are cleared if ($response->getStatusCode() === 400) { $this->clearCacheOnError($request); } if ($response->hasHeader('Content-Type')) { // Typically used when extbase for instance created a json response. $request->getAttribute('frontend.page.parts')->setHttpContentType($response->getHeaderLine('Content-Type')); // Do not send the header directly (see below) $response = $response->withoutHeader('Content-Type'); } $responseData = $request->getAttribute('frontend.response.data'); if ($responseData instanceof ResponseData) { foreach ($response->getHeaders() as $name => $values) { $responseData->setHeader($name, $values); } // @todo: Get rid of this in TYPO3 v15. See todos in ResponseData. if ($response->getStatusCode() >= 300) { $responseData->setProtocolVersion($response->getProtocolVersion()); $responseData->setStatusCode($response->getStatusCode()); $responseData->setReasonPhrase($response->getReasonPhrase()); } } $body = $response->getBody(); $body->rewind(); $content = $body->getContents(); $this->resetSingletons(); $this->cacheService->clearCachesOfRegisteredPageIds(); return $content; } /** * Entrypoint for backend modules, handling PSR-7 requests/responses. * * Creates an Extbase Request, dispatches it and then returns the Response * * @internal */ public function handleBackendRequest(ServerRequestInterface $request): ResponseInterface { // build the configuration from the module, included in the current request $module = $request->getAttribute('module'); $configuration = [ 'extensionName' => $module?->getExtensionName(), 'pluginName' => $module?->getIdentifier(), ]; $request = $this->initialize($configuration, $request); $extbaseRequest = $this->extbaseRequestBuilder->build($request); $response = $this->dispatcher->dispatch($extbaseRequest); $this->resetSingletons(); $this->cacheService->clearCachesOfRegisteredPageIds(); return $response; } /** * Clear cache of current page on error. Needed because we want a re-evaluation of the data. */ protected function clearCacheOnError(ServerRequestInterface $request): void { $extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK); if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') { $pageId = $request->getAttribute('frontend.page.information')?->getId(); if ($pageId !== null) { $this->cacheService->clearPageCache([$pageId]); } } } /** * Resets global singletons for the next plugin */ protected function resetSingletons(): void { $this->persistenceManager->persistAll(); } protected function isExtbaseRequestCacheable(RequestInterface $extbaseRequest): bool { $controllerClassName = $extbaseRequest->getControllerObjectName(); $actionName = $extbaseRequest->getControllerActionName(); $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK); $nonCacheableActions = $frameworkConfiguration['controllerConfiguration'][$controllerClassName]['nonCacheableActions'] ?? null; if (!is_array($nonCacheableActions)) { return true; } return !in_array($actionName, $nonCacheableActions, true); } }