diff --git a/core/modules/views/src/Plugin/views/display/Feed.php b/core/modules/views/src/Plugin/views/display/Feed.php index 16d40813005..caf6f8cbc92 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 @@ class Feed extends PathPluginBase implements ResponseDisplayPluginInterface { $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 ad8c30dfe6c..e59051dba68 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 @@ class Opml extends StylePluginBase { '#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 f8973550643..51394feda32 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 @@ class Rss extends StylePluginBase { '#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 f2510c690bf..6b6dd74019f 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 @@ class DisplayFeedTest extends ViewTestBase { $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 be6bfee2e4b..e048c714739 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'); - } } /**