From b20ec5185ccb60c7eb91c648a89c89a77c3899f2 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 27 Nov 2023 23:03:43 +0000
Subject: [PATCH] Issue #3272985 by recrit, grasmash, catch, smustgrave, xjm:
 RSS Feed header reverts to text/html when cached

---
 .../views/src/Plugin/views/display/Feed.php   | 38 +++++++++++++++++++
 .../views/src/Plugin/views/style/Opml.php     |  5 +++
 .../views/src/Plugin/views/style/Rss.php      |  5 +++
 .../src/Functional/Plugin/DisplayFeedTest.php | 34 +++++++++++++++++
 core/modules/views/views.theme.inc            | 12 ------
 5 files changed, 82 insertions(+), 12 deletions(-)

diff --git a/core/modules/views/src/Plugin/views/display/Feed.php b/core/modules/views/src/Plugin/views/display/Feed.php
index 16d40813005a..caf6f8cbc92f 100644
--- a/core/modules/views/src/Plugin/views/display/Feed.php
+++ b/core/modules/views/src/Plugin/views/display/Feed.php
@@ -116,9 +116,47 @@ public static function buildResponse($view_id, $display_id, array $args = []) {
     $cache_metadata = CacheableMetadata::createFromRenderArray($build);
     $response->addCacheableDependency($cache_metadata);
 
+    // Set the HTTP headers and status code on the response if any bubbled.
+    if (!empty($build['#attached']['http_header'])) {
+      static::setHeaders($response, $build['#attached']['http_header']);
+    }
+
     return $response;
   }
 
+  /**
+   * Sets headers on a response object.
+   *
+   * @param \Drupal\Core\Cache\CacheableResponse $response
+   *   The HTML response to update.
+   * @param array $headers
+   *   The headers to set, as an array. The items in this array should be as
+   *   follows:
+   *   - The header name.
+   *   - The header value.
+   *   - (optional) Whether to replace a current value with the new one, or add
+   *     it to the others. If the value is not replaced, it will be appended,
+   *     resulting in a header like this: 'Header: value1,value2'.
+   *
+   * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor::setHeaders()
+   */
+  protected static function setHeaders(CacheableResponse $response, array $headers): void {
+    foreach ($headers as $values) {
+      $name = $values[0];
+      $value = $values[1];
+      $replace = !empty($values[2]);
+
+      // Drupal treats the HTTP response status code like a header, even though
+      // it really is not.
+      if (strtolower($name) === 'status') {
+        $response->setStatusCode($value);
+      }
+      else {
+        $response->headers->set($name, $value, $replace);
+      }
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/views/src/Plugin/views/style/Opml.php b/core/modules/views/src/Plugin/views/style/Opml.php
index ad8c30dfe6cb..e59051dba68d 100644
--- a/core/modules/views/src/Plugin/views/style/Opml.php
+++ b/core/modules/views/src/Plugin/views/style/Opml.php
@@ -67,6 +67,11 @@ public function render() {
       '#view' => $this->view,
       '#options' => $this->options,
       '#rows' => $rows,
+      '#attached' => [
+        'http_header' => [
+          ['Content-Type', 'text/xml; charset=utf-8'],
+        ],
+      ],
     ];
     unset($this->view->row_index);
     return $build;
diff --git a/core/modules/views/src/Plugin/views/style/Rss.php b/core/modules/views/src/Plugin/views/style/Rss.php
index f8973550643f..51394feda329 100644
--- a/core/modules/views/src/Plugin/views/style/Rss.php
+++ b/core/modules/views/src/Plugin/views/style/Rss.php
@@ -132,6 +132,11 @@ public function render() {
       '#view' => $this->view,
       '#options' => $this->options,
       '#rows' => $rows,
+      '#attached' => [
+        'http_header' => [
+          ['Content-Type', 'application/rss+xml; charset=utf-8'],
+        ],
+      ],
     ];
     unset($this->view->row_index);
     return $build;
diff --git a/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php b/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php
index f2510c690bf8..6b6dd74019f2 100644
--- a/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php
+++ b/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php
@@ -204,4 +204,38 @@ public function testDisabledLinkedDisplay() {
     $this->assertSession()->statusCodeEquals(200);
   }
 
+  /**
+   * Tests the cacheability of the feed display.
+   */
+  public function testFeedCacheability(): void {
+    // Test as an anonymous user.
+    $this->drupalLogout();
+
+    // Set the page cache max age to a value greater than zero.
+    $config = $this->config('system.performance');
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+
+    // Uninstall all page cache modules that could cache the HTTP response
+    // headers.
+    \Drupal::service('module_installer')->uninstall([
+      'page_cache',
+      'dynamic_page_cache',
+    ]);
+
+    // Reset all so that the config and module changes are active.
+    $this->resetAll();
+
+    $url = 'test-feed-display.xml';
+    $this->drupalGet($url);
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->responseHeaderEquals('Cache-Control', 'max-age=300, public');
+    $this->assertSession()->responseHeaderEquals('Content-Type', 'application/rss+xml; charset=utf-8');
+
+    // Visit the page again to get the cached response.
+    $this->drupalGet($url);
+    $this->assertSession()->responseHeaderEquals('Cache-Control', 'max-age=300, public');
+    $this->assertSession()->responseHeaderEquals('Content-Type', 'application/rss+xml; charset=utf-8');
+  }
+
 }
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index be6bfee2e4b9..e048c7147390 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -928,12 +928,6 @@ function template_preprocess_views_view_rss(&$variables) {
   $variables['namespaces'] = new Attribute($style->namespaces);
   $variables['items'] = $items;
   $variables['channel_elements'] = $style->channel_elements;
-
-  // During live preview we don't want to output the header since the contents
-  // of the feed are being displayed inside a normal HTML page.
-  if (empty($variables['view']->live_preview)) {
-    $variables['view']->getResponse()->headers->set('Content-Type', 'application/rss+xml; charset=utf-8');
-  }
 }
 
 /**
@@ -995,12 +989,6 @@ function template_preprocess_views_view_opml(&$variables) {
   $variables['title'] = $title;
   $variables['items'] = $items;
   $variables['updated'] = gmdate(DATE_RFC2822, \Drupal::time()->getRequestTime());
-
-  // During live preview we don't want to output the header since the contents
-  // of the feed are being displayed inside a normal HTML page.
-  if (empty($variables['view']->live_preview)) {
-    $variables['view']->getResponse()->headers->set('Content-Type', 'text/xml; charset=utf-8');
-  }
 }
 
 /**
-- 
GitLab