From ecc1ffd024541e0fb31da6510b114eb78a4fe579 Mon Sep 17 00:00:00 2001
From: Helmut Hummel <typo3@helhum.io>
Date: Wed, 14 Dec 2022 18:55:50 +0100
Subject: [PATCH] [BUGFIX] Send correct content type for cached Extbase actions

The setContentType method on TyposcripFrontendController
was introduced to allow Extbase plugins to change the
content-type header of the server http response.

However this currently only works, when the Extbase plugin
action is uncached. Once a plugin, that is rendered on a page
ist cached in page cache, the content type of subsequent
requests is always "text/html", because the value of the
contentType property is not stored in cache.

Storing this value into cache allows creating e.g.
Json APIs as Extbase Plugins, that can be fully cached
without adding hacks like modifying config.additionalHeaders

Resolves: #99373
Releases: main, 11.5
Change-Id: Ibf00c9438d8763ef9d32c6ad7d00a44d3137ba13
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77269
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Helmut Hummel <typo3@helhum.io>
Tested-by: Helmut Hummel <typo3@helhum.io>
---
 .../Classes/Controller/QueueController.php    |  5 +----
 .../Classes/Controller/QueueController.php    |  5 +----
 .../sysext/extbase/Classes/Core/Bootstrap.php | 20 +++++--------------
 .../TypoScriptFrontendController.php          |  5 ++++-
 4 files changed, 11 insertions(+), 24 deletions(-)

diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/Classes/Controller/QueueController.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/Classes/Controller/QueueController.php
index 6dd96e729b76..991647c89c72 100644
--- a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/Classes/Controller/QueueController.php
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/Classes/Controller/QueueController.php
@@ -90,13 +90,10 @@ class QueueController extends ActionController
 
     public function finishAction(): ResponseInterface
     {
-        $typoScriptFrontendController = $GLOBALS['TSFE'];
-        $typoScriptFrontendController->setContentType('application/json');
-
         $value = $this->queueService->getValues();
         $this->view->assign('value', $value);
         $body = new Stream('php://temp', 'rw');
         $body->write($this->view->render());
-        return new Response($body);
+        return (new Response($body))->withHeader('Content-Type', 'application/json; charset=utf-8');
     }
 }
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_irre_foreignfield/Classes/Controller/QueueController.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_irre_foreignfield/Classes/Controller/QueueController.php
index c221eb3654ae..8b518e6c6f14 100644
--- a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_irre_foreignfield/Classes/Controller/QueueController.php
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_irre_foreignfield/Classes/Controller/QueueController.php
@@ -90,13 +90,10 @@ class QueueController extends ActionController
 
     public function finishAction(): ResponseInterface
     {
-        $typoScriptFrontendController = $GLOBALS['TSFE'];
-        $typoScriptFrontendController->setContentType('application/json');
-
         $value = $this->queueService->getValues();
         $this->view->assign('value', $value);
         $body = new Stream('php://temp', 'rw');
         $body->write($this->view->render());
-        return new Response($body);
+        return (new Response($body))->withHeader('Content-Type', 'application/json; charset=utf-8');
     }
 }
diff --git a/typo3/sysext/extbase/Classes/Core/Bootstrap.php b/typo3/sysext/extbase/Classes/Core/Bootstrap.php
index a65100315c8a..2b9b79b8bcbe 100644
--- a/typo3/sysext/extbase/Classes/Core/Bootstrap.php
+++ b/typo3/sysext/extbase/Classes/Core/Bootstrap.php
@@ -176,24 +176,14 @@ class Bootstrap
             $this->clearCacheOnError();
         }
 
-        // In case TSFE is available and this is a json response, we have
-        // to take the TypoScript settings regarding charset into account.
-        // @todo Since HTML5 only utf-8 is a valid charset, this settings should be deprecated
+        // In case TSFE is available and this is a json response, we have to let TSFE know we have a specific Content-Type
         if (($typoScriptFrontendController = ($GLOBALS['TSFE'] ?? null)) instanceof TypoScriptFrontendController
-            && strpos($response->getHeaderLine('Content-Type'), 'application/json') === 0
+            && $response->hasHeader('Content-Type')
         ) {
-            // Unset the already defined Content-Type
+            [$contentType] = explode(';', $response->getHeaderLine('Content-Type'));
+            // Do not send the header directly (see below)
             $response = $response->withoutHeader('Content-Type');
-            if (empty($typoScriptFrontendController->config['config']['disableCharsetHeader'])) {
-                // If the charset header is *not* disabled in configuration,
-                // TypoScriptFrontendController will send the header later with the Content-Type which we set here.
-                $typoScriptFrontendController->setContentType('application/json');
-            } else {
-                // Although the charset header is disabled in configuration, we *must* send a Content-Type header here.
-                // Content-Type headers optionally carry charset information at the same time.
-                // Since we have the information about the charset, there is no reason to not include the charset information although disabled in TypoScript.
-                $response = $response->withHeader('Content-Type', 'application/json; charset=' . trim($typoScriptFrontendController->metaCharset));
-            }
+            $typoScriptFrontendController->setContentType($contentType);
         }
 
         if (headers_sent() === false) {
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index b2672237a7aa..3e46d116c810 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -663,7 +663,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
 
     /**
      * @param string $contentType
-     * @internal Should only be used by TYPO3 core for now
+     * @internal Must only be used by TYPO3 core
      */
     public function setContentType($contentType)
     {
@@ -1676,6 +1676,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $this->config = $cachedData['cache_data'];
         // Getting the content
         $this->content = $cachedData['content'];
+        // Getting the content type
+        $this->contentType = $cachedData['contentType'] ?? $this->contentType;
         // Setting flag, so we know, that some cached content has been loaded
         $this->cacheContentFlag = true;
         $this->cacheExpires = $cachedData['expires'];
@@ -2374,6 +2376,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             'identifier' => $this->newHash,
             'page_id' => $this->id,
             'content' => $content,
+            'contentType' => $this->contentType,
             'cache_data' => $data,
             'expires' => $expirationTstamp,
             'tstamp' => $GLOBALS['EXEC_TIME'],
-- 
GitLab