From 09ff81ae521ffe69c816015480550117369d7358 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Wed, 29 Jan 2014 10:47:12 +0000
Subject: [PATCH] Issue #2167039 by likin, dawehner, Wim Leers, tstoeckler,
 vijaycs85: Regression: page cache tags broken, also: some routes incorrectly
 use _controller -> No page object in template variables -> Fatal in
 seven_preprocess_page().

---
 core/includes/common.inc                      | 33 +++--------
 .../Drupal/Core/Cache/CacheableInterface.php  | 54 +++++++++++++++++
 .../Core/Controller/HtmlControllerBase.php    | 22 ++++++-
 .../EventSubscriber/HtmlViewSubscriber.php    | 12 ++++
 .../Core/Page/DefaultHtmlPageRenderer.php     | 16 +++--
 core/lib/Drupal/Core/Page/HtmlFragment.php    | 58 ++++++++++++++++++-
 core/lib/Drupal/Core/Page/HtmlPage.php        |  6 +-
 .../modules/aggregator/aggregator.routing.yml |  2 +-
 .../config_translation/ConfigNamesMapper.php  |  2 +-
 .../Tests/ConfigNamesMapperTest.php           |  2 +-
 .../Drupal/node/Tests/NodePageCacheTest.php   |  9 ++-
 core/modules/simpletest/simpletest.module     |  1 +
 .../system/Controller/BatchController.php     |  2 +-
 .../system/Tests/Bootstrap/PageCacheTest.php  |  7 +++
 core/modules/system/system.module             |  1 -
 .../system_test/system_test.routing.yml       |  2 +-
 16 files changed, 188 insertions(+), 41 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Cache/CacheableInterface.php

diff --git a/core/includes/common.inc b/core/includes/common.inc
index 580ef214ad5d..b97cc5e50945 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3147,7 +3147,7 @@ function drupal_page_set_cache(Response $response, Request $request) {
         // because by the time it is read, the configuration might change.
         'page_compressed' => $page_compressed,
       ),
-      'tags' => array('content' => TRUE) + drupal_cache_tags_page_get(),
+      'tags' => array('content' => TRUE) + drupal_cache_tags_page_get($response),
       'expire' => Cache::PERMANENT,
       'created' => REQUEST_TIME,
     );
@@ -4358,36 +4358,21 @@ function drupal_render_collect_cache_tags($element, $tags = array()) {
   return $tags;
 }
 
-/**
- * A #post_render callback at the top level of the $page array. Collects the
- * tags for use in page cache.
- *
- * @param string $children
- *   An HTML string of rendered output.
- * @param array $elements
- *   A render array.
- *
- * @return string
- *   The same $children that was passed in - no modifications.
- */
-function drupal_post_render_cache_tags_page_set($children, array $elements) {
-  if (drupal_page_is_cacheable()) {
-    $tags = &drupal_static('system_cache_tags_page', array());
-    $tags = drupal_render_collect_cache_tags($elements);
-  }
-  return $children;
-}
-
 /**
  * Return the cache tags that were stored during drupal_render_page().
  *
+ * @param \Symfony\Component\HttpFoundation\Response $response
+ *   The response object.
  * @return array
  *   An array of cache tags.
  *
- * @see drupal_post_render_cache_tags_page_set()
+ * @see \Drupal\Core\EventSubscriber\HtmlViewSubscriber::onHtmlPage()
  */
-function drupal_cache_tags_page_get() {
-  return drupal_static('system_cache_tags_page', array());
+function drupal_cache_tags_page_get(Response $response) {
+  if (($tags = $response->headers->get('cache_tags')) && $tags = unserialize($tags)) {
+    return $tags;
+  }
+  return array();
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Cache/CacheableInterface.php b/core/lib/Drupal/Core/Cache/CacheableInterface.php
new file mode 100644
index 000000000000..da4f04c2acdc
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableInterface.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\CacheableInterface
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Defines an interface for objects which are potentially cacheable.
+ */
+interface CacheableInterface {
+
+  /**
+   * The cache keys associated with this potentially cacheable object.
+   *
+   * @return array
+   *   An array of strings or cache constants, used to generate a cache ID.
+   */
+  public function getCacheKeys();
+
+  /**
+   * The cache tags associated with this potentially cacheable object.
+   *
+   * @return array
+   *  An array of cache tags.
+   */
+  public function getCacheTags();
+
+  /**
+   * The bin to use for this potentially cacheable object.
+   *
+   * @return string
+   *   The name of the cache bin to use.
+   */
+  public function getCacheBin();
+
+  /**
+   * The maximum age for which this object may be cached.
+   *
+   * @return int
+   *   The maximum time in seconds that this object may be cached.
+   */
+  public function getCacheMaxAge();
+
+  /**
+   * Indicates whether this object is cacheable.
+   *
+   * @return bool
+   *   Returns TRUE if the object is cacheable, FALSE otherwise.
+   */
+  public function isCacheable();
+
+}
diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
index bf7be8df3c20..0204bc06e1e9 100644
--- a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
+++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
@@ -71,7 +71,9 @@ protected function createHtmlFragment($page_content, Request $request) {
       );
     }
 
-    $fragment = new HtmlFragment(drupal_render($page_content));
+    $cache_tags = $this->drupalRenderCollectCacheTags($page_content);
+    $cache = !empty($cache_tags) ? array('tags' => $cache_tags) : array();
+    $fragment = new HtmlFragment($this->drupalRender($page_content), $cache);
 
     // A title defined in the return always wins.
     if (isset($page_content['#title'])) {
@@ -84,4 +86,22 @@ protected function createHtmlFragment($page_content, Request $request) {
     return $fragment;
   }
 
+  /**
+   * Wraps drupal_render().
+   *
+   * @todo: Remove as part of https://drupal.org/node/2182149
+   */
+  protected function drupalRender(&$elements, $is_recursive_call = FALSE) {
+    return drupal_render($elements, $is_recursive_call);
+  }
+
+  /**
+   * Wraps drupal_render_collect_cache_tags()
+   *
+   * @todo: Remove as part of https://drupal.org/node/2182149
+   */
+  protected function drupalRenderCollectCacheTags($element, $tags = array()) {
+    return drupal_render_collect_cache_tags($element, $tags);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
index f66b6040f0cb..ae7c9a1b7465 100644
--- a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php
@@ -65,6 +65,18 @@ public function onHtmlPage(GetResponseForControllerResultEvent $event) {
       // to return an object implementing __toString(), but that is not
       // recommended.
       $response = new Response((string) $this->renderer->renderPage($page), $page->getStatusCode());
+      if ($tags = $page->getCacheTags()) {
+        $response->headers->set('cache_tags', serialize($tags));
+      }
+      if ($keys = $page->getCacheKeys()) {
+        $response->headers->set('cache_keys', serialize($keys));
+      }
+      if ($bin = $page->getCacheBin()) {
+        $response->headers->set('cache_bin', $bin);
+      }
+      if ($max_age = $page->getCacheMaxAge()) {
+        $response->headers->set('cache_max_age', $max_age);
+      }
       $event->setResponse($response);
     }
   }
diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php
index ad29a5b08403..0d3b6d50d696 100644
--- a/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php
+++ b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php
@@ -36,21 +36,27 @@ public function __construct(LanguageManager $language_manager) {
    * {@inheritdoc}
    */
   public function render(HtmlFragment $fragment, $status_code = 200) {
-    $page = new HtmlPage('', $fragment->getTitle());
-
+    // Converts the given HTML fragment which represents the main content region
+    // of the page into a render array.
     $page_content['main'] = array(
       '#markup' => $fragment->getContent(),
+      '#cache' => array('tags' => $fragment->getCacheTags()),
     );
-    $page_content['#title'] = $page->getTitle();
+    $page_content['#title'] = $fragment->getTitle();
 
+    // Build the full page array by calling drupal_prepare_page(), which invokes
+    // hook_page_build(). This adds the other regions to the page.
     $page_array = drupal_prepare_page($page_content);
 
-    $page = $this->preparePage($page, $page_array);
+    // Collect cache tags for all the content in all the regions on the page.
+    $tags = drupal_render_collect_cache_tags($page_array);
 
+    // Build the HtmlPage object.
+    $page = new HtmlPage('', array('tags' => $tags), $fragment->getTitle());
+    $page = $this->preparePage($page, $page_array);
     $page->setBodyTop(drupal_render($page_array['page_top']));
     $page->setBodyBottom(drupal_render($page_array['page_bottom']));
     $page->setContent(drupal_render($page_array));
-
     $page->setStatusCode($status_code);
 
     return $page;
diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php
index 3a50d5e9707f..cc5cac5a6c4e 100644
--- a/core/lib/Drupal/Core/Page/HtmlFragment.php
+++ b/core/lib/Drupal/Core/Page/HtmlFragment.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\Cache\CacheableInterface;
 use Drupal\Core\Utility\Title;
 
 /**
@@ -18,7 +19,7 @@
  *   https://drupal.org/node/1871596#comment-7134686
  * @todo Add method replacements for *all* data sourced by html.tpl.php.
  */
-class HtmlFragment {
+class HtmlFragment implements CacheableInterface {
 
   /**
    * HTML content string.
@@ -34,14 +35,30 @@ class HtmlFragment {
    */
   protected $title = '';
 
+  /**
+   * The cache metadata of this HtmlFragment.
+   *
+   * @var array
+   */
+  protected $cache = array();
+
   /**
    * Constructs a new HtmlFragment.
    *
    * @param string $content
    *   The content for this fragment.
+   * @param array $cache_info
+   *   The cache information.
    */
-  public function __construct($content = '') {
+  public function __construct($content = '', array $cache_info = array()) {
     $this->content = $content;
+    $this->cache = $cache_info + array(
+      'keys' => array(),
+      'tags' => array(),
+      'bin' => NULL,
+      'max_age' => 0,
+      'is_cacheable' => TRUE,
+    );
   }
 
   /**
@@ -123,4 +140,41 @@ public function getTitle() {
     return $this->title;
   }
 
+  /**
+   * {@inheritdoc}
+   *
+   * @TODO Use a trait once we require php 5.4 for all the cache methods.
+   */
+  public function getCacheKeys() {
+    return $this->cache['keys'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->cache['tags'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheBin() {
+    return $this->cache['bin'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->cache['max_age'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return $this->cache['is_cacheable'];
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php
index 6996d7c1e171..c2f09edc5a4b 100644
--- a/core/lib/Drupal/Core/Page/HtmlPage.php
+++ b/core/lib/Drupal/Core/Page/HtmlPage.php
@@ -54,11 +54,13 @@ class HtmlPage extends HtmlFragment {
    *
    * @param string $content
    *   (optional) The body content of the page.
+   * @param array $cache_info
+   *   The cache information.
    * @param string $title
    *   (optional) The title of the page.
    */
-  public function __construct($content = '', $title = '') {
-    parent::__construct($content);
+  public function __construct($content = '', array $cache_info = array(), $title = '') {
+    parent::__construct($content, $cache_info);
 
     $this->title = $title;
 
diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml
index 33ac34dd035a..1f3464deebc1 100644
--- a/core/modules/aggregator/aggregator.routing.yml
+++ b/core/modules/aggregator/aggregator.routing.yml
@@ -57,7 +57,7 @@ aggregator.feed_edit:
 aggregator.feed_refresh:
   path: '/admin/config/services/aggregator/update/{aggregator_feed}'
   defaults:
-    _controller: '\Drupal\aggregator\Controller\AggregatorController::feedRefresh'
+    _content: '\Drupal\aggregator\Controller\AggregatorController::feedRefresh'
     _title: 'Update items'
   requirements:
     _permission: 'administer news feeds'
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php
index 2df738732e32..7ba5b2a2162a 100644
--- a/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php
@@ -176,7 +176,7 @@ public function getOverviewRoute() {
     return new Route(
       $this->getBaseRoute()->getPath() . '/translate',
       array(
-        '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
+        '_content' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
         'plugin_id' => $this->getPluginId(),
       ),
       array('_config_translation_overview_access' => 'TRUE')
diff --git a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php
index 3df6ac024500..ac8a13a30bba 100644
--- a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php
+++ b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php
@@ -176,7 +176,7 @@ public function testGetOverviewRouteParameters() {
   public function testGetOverviewRoute() {
     $expected = new Route('/admin/config/system/site-information/translate',
       array(
-        '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
+        '_content' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
         'plugin_id' => 'system.site_information_settings',
       ),
       array(
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php b/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php
index 4a2385cd0119..49bc190faed8 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php
@@ -48,11 +48,18 @@ function setUp() {
    * Tests deleting nodes clears page cache.
    */
   public function testNodeDelete() {
-    $node_path = 'node/' . $this->drupalCreateNode()->id();
+    $author = $this->drupalCreateUser();
+    $node_path = 'node/' . $this->drupalCreateNode(array('uid' => $author->id()))->id();
 
     // Populate page cache.
     $this->drupalGet($node_path);
 
+    // Verify the presence of the correct cache tags.
+    $cid_parts = array(url($node_path, array('absolute' => TRUE)), 'html');
+    $cid = sha1(implode(':', $cid_parts));
+    $cache_entry = \Drupal::cache('page')->get($cid);
+    $this->assertIdentical($cache_entry->tags, array('content:1', 'user:' . $author->id(), 'filter_format:plain_text'));
+
     // Login and delete the node.
     $this->drupalLogin($this->adminUser);
     $this->drupalGet($node_path . '/delete');
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index 6efecef52513..c6b53bf0c405 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -1,6 +1,7 @@
 <?php
 
 use Drupal\Core\Database\Database;
+use Drupal\Core\Page\HtmlPage;
 use Drupal\simpletest\TestBase;
 use Symfony\Component\Process\PhpExecutableFinder;
 
diff --git a/core/modules/system/lib/Drupal/system/Controller/BatchController.php b/core/modules/system/lib/Drupal/system/Controller/BatchController.php
index 2e7480d6af84..340cd75a059a 100644
--- a/core/modules/system/lib/Drupal/system/Controller/BatchController.php
+++ b/core/modules/system/lib/Drupal/system/Controller/BatchController.php
@@ -102,7 +102,7 @@ public function render(array $output, $status_code = 200) {
       $request = \Drupal::request();
       $output['#title'] = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
     }
-    $page = new HtmlPage('', $output['#title']);
+    $page = new HtmlPage('', isset($output['#cache']) ? $output['#cache'] : array(), $output['#title']);
 
     $page_array = drupal_prepare_page($output);
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
index aec3355fca71..3ca1f19f34d3 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
@@ -58,8 +58,15 @@ function testPageCacheTags() {
     $tags = array('system_test_cache_tags_page' => TRUE);
     $this->drupalGet($path);
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Verify a cache hit, but also the presence of the correct cache tags.
     $this->drupalGet($path);
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+    $cid_parts = array(url($path, array('absolute' => TRUE)), 'html');
+    $cid = sha1(implode(':', $cid_parts));
+    $cache_entry = \Drupal::cache('page')->get($cid);
+    $this->assertIdentical($cache_entry->tags, array('content:1', 'system_test_cache_tags_page:1'));
+
     Cache::invalidateTags($tags);
     $this->drupalGet($path);
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 4f92d7352cde..cc647a23c9e5 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -269,7 +269,6 @@ function system_element_info() {
     '#theme_wrappers' => array('form'),
   );
   $types['page'] = array(
-    '#post_render' => array('drupal_post_render_cache_tags_page_set'),
     '#show_messages' => TRUE,
     '#theme' => 'page',
   );
diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml
index ecd03ed59c59..0a6ee6f555ad 100644
--- a/core/modules/system/tests/modules/system_test/system_test.routing.yml
+++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml
@@ -48,7 +48,7 @@ system_test.lock_exit:
 system_test.cache_tags_page:
   path: '/system-test/cache_tags_page'
   defaults:
-    _controller: '\Drupal\system_test\Controller\SystemTestController::system_test_cache_tags_page'
+    _content: '\Drupal\system_test\Controller\SystemTestController::system_test_cache_tags_page'
   requirements:
     _access: 'TRUE'
 
-- 
GitLab