 core/includes/common.inc                           |   13 +++
 core/includes/menu.inc                             |   41 ++++----
 .../menu/lib/Drupal/menu/Tests/MenuTest.php        |   60 +++++++++++
 .../lib/Drupal/menu_link/Entity/MenuLink.php       |   13 ++-
 .../system/lib/Drupal/system/Entity/Menu.php       |   19 ++++
 .../Tests/Cache/PageCacheTagsIntegrationTest.php   |  105 ++++++++++++++++++++
 6 files changed, 228 insertions(+), 23 deletions(-)

diff --git a/core/includes/common.inc b/core/includes/common.inc
index b97cc5e..ff13861 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3562,6 +3562,19 @@ function drupal_prepare_page($page) {
   // 'sidebar_first', 'footer', etc.
   drupal_alter('page', $page);
 
+  // The "main" and "secondary" menus are never part of the page-level render
+  // array and therefor their cache tags will never bubble up into the page
+  // cache, even though they should be. This happens because they're rendered
+  // directly by the theme system.
+  if (theme_get_setting('features.main_menu') && count(menu_main_menu())) {
+    $main_links_source = _menu_get_links_source('main_links', 'main');
+    $page['page_top']['#cache']['tags']['menu'][$main_links_source] = $main_links_source;
+  }
+  if (theme_get_setting('features.secondary_menu') && count(menu_secondary_menu())) {
+    $secondary_links_source = _menu_get_links_source('secondary_links', 'account');
+    $page['page_top']['#cache']['tags']['menu'][$secondary_links_source] = $secondary_links_source;
+  }
+
   // If no module has taken care of the main content, add it to the page now.
   // This allows the site to still be usable even if no modules that
   // control page regions (for example, the Block module) are enabled.
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index bbf5c43..814c9d9 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -1100,6 +1100,9 @@ function menu_tree_output($tree) {
     // Add the theme wrapper for outer markup.
     // Allow menu-specific theme overrides.
     $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_');
+    // Set cache tag.
+    $menu_name = $data['link']['menu_name'];
+    $build['#cache']['tags']['menu'][$menu_name] = $menu_name;
   }
 
   return $build;
@@ -1758,10 +1761,7 @@ function menu_list_system_menus() {
  * Returns an array of links to be rendered as the Main menu.
  */
 function menu_main_menu() {
-  $config = \Drupal::config('menu.settings');
-  $menu_enabled = module_exists('menu');
-  // When menu module is not enabled, we need a hardcoded default value.
-  $main_links_source = $menu_enabled ? $config->get('main_links') : 'main';
+  $main_links_source = _menu_get_links_source('main_links', 'main');
   return menu_navigation_links($main_links_source);
 }
 
@@ -1769,11 +1769,8 @@ function menu_main_menu() {
  * Returns an array of links to be rendered as the Secondary links.
  */
 function menu_secondary_menu() {
-  $config = \Drupal::config('menu.settings');
-  $menu_enabled = module_exists('menu');
-  // When menu module is not enabled, we need a hardcoded default value.
-  $main_links_source = $menu_enabled ? $config->get('main_links') : 'main';
-  $secondary_links_source = $menu_enabled ? $config->get('secondary_links') : 'account';
+  $main_links_source = _menu_get_links_source('main_links', 'main');
+  $secondary_links_source = _menu_get_links_source('secondary_links', 'account');
 
   // If the secondary menu source is set as the primary menu, we display the
   // second level of the primary menu.
@@ -1786,6 +1783,23 @@ function menu_secondary_menu() {
 }
 
 /**
+ * Returns the source of links of a menu.
+ *
+ * @param string $name
+ *   A string configuration key of menu link source.
+ * @param string $default
+ *   Default menu name.
+ *
+ * @return string
+ *   Returns menu name, if exist
+ */
+function _menu_get_links_source($name, $default) {
+  $config = \Drupal::config('menu.settings');
+  // When menu module is not enabled, we need a hardcoded default value.
+  return \Drupal::moduleHandler()->moduleExists('menu') ? $config->get($name) : $default;
+}
+
+/**
  * Returns an array of links for a navigation menu.
  *
  * @param $menu_name
@@ -2404,15 +2418,6 @@ function menu_get_active_trail() {
 }
 
 /**
- * Clears the cached cached data for a single named menu.
- */
-function menu_cache_clear($menu_name = 'tools') {
-  Cache::deleteTags(array('menu' => $menu_name));
-  // Also clear the menu system static caches.
-  menu_reset_static_cache();
-}
-
-/**
  * Clears all cached menu data.
  *
  * This should be called any time broad changes
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index 48f9361..d03012d 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
@@ -457,6 +457,66 @@ public function testBlockContextualLinks() {
   }
 
   /**
+   * Test that cache tags are properly set and bubbled up to the page cache.
+   *
+   * Ensures that invalidation of the "menu:<menu name>" cache tags works.
+   */
+  public function testMenuBlockPageCacheTags() {
+    // Enable page caching.
+    $config = \Drupal::config('system.performance');
+    $config->set('cache.page.use_internal', 1);
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+
+    // Create a Llama menu, add a link to it and place the corresponding block.
+    $menu = entity_create('menu', array(
+      'id' => 'llama',
+      'label' => 'Llama',
+      'description' => 'Description text',
+    ));
+    $menu->save();
+    $menu_link = entity_create('menu_link', array(
+      'link_path' => '<front>',
+      'link_title' => 'Vicuña',
+      'menu_name' => 'llama',
+    ));
+    $menu_link->save();
+    $block = $this->drupalPlaceBlock('system_menu_block:llama', array('label' => 'Llama', 'module' => 'system', 'region' => 'footer'));
+
+    // Prime the page cache.
+    $this->drupalGet('test-page');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Verify a cache hit, but also the presence of the correct cache tags.
+    $this->drupalGet('test-page');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+    $cid_parts = array(url('test-page', array('absolute' => TRUE)), 'html');
+    $cid = sha1(implode(':', $cid_parts));
+    $cache_entry = \Drupal::cache('page')->get($cid);
+    $this->assertIdentical($cache_entry->tags, array('content:1', 'menu:llama'));
+
+    // The "Llama" menu is modified.
+    $menu->label = 'Awesome llama';
+    $menu->save();
+
+    // Verify that after the modified menu, there is a cache miss.
+    $this->drupalGet('test-page');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Verify a cache hit.
+    $this->drupalGet('test-page');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+
+    // A link in the "Llama" menu is modified.
+    $menu_link->link_title = 'Guanaco';
+    $menu_link->save();
+
+    // Verify that after the modified menu link, there is a cache miss.
+    $this->drupalGet('test-page');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+  }
+
+  /**
    * Tests menu link bundles.
    */
   public function testMenuBundles() {
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php b/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php
index 6d70014..be72585 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\menu_link\Entity;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\Entity;
 use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\menu_link\MenuLinkInterface;
@@ -446,9 +447,9 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
       }
     }
 
-    foreach ($affected_menus as $menu_name) {
-      menu_cache_clear($menu_name);
-    }
+    Cache::invalidateTags(array('menu' => array_keys($affected_menus)));
+    // Also clear the menu system static caches.
+    menu_reset_static_cache();
     _menu_clear_page_cache();
   }
 
@@ -527,10 +528,12 @@ public function postSave(EntityStorageControllerInterface $storage_controller, $
     // Check the has_children status of the parent.
     $storage_controller->updateParentalStatus($this);
 
-    menu_cache_clear($this->menu_name);
+    Cache::invalidateTags(array('menu' => $this->menu_name));
     if (isset($this->original) && $this->menu_name != $this->original->menu_name) {
-      menu_cache_clear($this->original->menu_name);
+      Cache::invalidateTags(array('menu' => $this->original->menu_name));
     }
+    // Also clear the menu system static caches.
+    menu_reset_static_cache();
 
     // Now clear the cache.
     _menu_clear_page_cache();
diff --git a/core/modules/system/lib/Drupal/system/Entity/Menu.php b/core/modules/system/lib/Drupal/system/Entity/Menu.php
index 644629a..6e8fce8 100644
--- a/core/modules/system/lib/Drupal/system/Entity/Menu.php
+++ b/core/modules/system/lib/Drupal/system/Entity/Menu.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Entity;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\system\MenuInterface;
@@ -90,4 +91,22 @@ public function isLocked() {
     return (bool) $this->locked;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    parent::postSave($storage_controller, $update);
+
+    Cache::invalidateTags(array('menu' => $this->id()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    parent::postDelete($storage_controller, $entities);
+
+    Cache::invalidateTags(array('menu' => array_keys($entities)));
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
new file mode 100644
index 0000000..4b4c799
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Cache\PageCacheTagsIntegrationTest.
+ */
+
+namespace Drupal\system\Tests\Cache;
+
+use Drupal\simpletest\WebTestBase;
+use Drupal\Core\Cache\Cache;
+
+/**
+ * Enables the page cache and tests its cache tags in various scenarios.
+ *
+ * @see \Drupal\system\Tests\Bootstrap\PageCacheTest
+ * @see \Drupal\node\Tests\NodePageCacheTest
+ * @see \Drupal\menu\Tests\MenuTest::testMenuBlockPageCacheTags()
+ */
+class PageCacheTagsIntegrationTest extends WebTestBase {
+
+  protected $profile = 'standard';
+
+  protected $dumpHeaders = TRUE;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Page cache tags integration test',
+      'description' => 'Enable the page cache and test its cache tags in various scenarios.',
+      'group' => 'Cache',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $config = \Drupal::config('system.performance');
+    $config->set('cache.page.use_internal', 1);
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+  }
+
+  /**
+   * Test that cache tags are properly bubbled up to the page level.
+   */
+  function testPageCacheTags() {
+    // Create two nodes.
+    $author_1 = $this->drupalCreateUser();
+    $node_1_path = 'node/' . $this->drupalCreateNode(array(
+      'uid' => $author_1->id(),
+      'title' => 'Node 1',
+      'body' => array(
+        0 => array('value' => 'Body 1', 'format' => 'basic_html'),
+      ),
+      'promote' => NODE_PROMOTED,
+    ))->id();
+    $author_2 = $this->drupalCreateUser();
+    $node_2_path = 'node/' . $this->drupalCreateNode(array(
+      'uid' => $author_2->id(),
+      'title' => 'Node 2',
+      'body' => array(
+        0 => array('value' => 'Body 2', 'format' => 'full_html'),
+      ),
+      'promote' => NODE_PROMOTED,
+    ))->id();
+
+    // Full node page 1.
+    $this->verifyPageCacheTags($node_1_path, array(
+      'content:1',
+      'user:' . $author_1->id(),
+      'filter_format:basic_html',
+      'menu:footer',
+      'menu:main',
+    ));
+
+    // Full node page 2.
+    $this->verifyPageCacheTags($node_2_path, array(
+      'content:1',
+      'user:' . $author_2->id(),
+      'filter_format:full_html',
+      'menu:footer',
+      'menu:main',
+    ));
+  }
+
+  /**
+   * Fills page cache for the given path, verify cache tags on page cache hit.
+   *
+   * @param $path
+   *   The Drupal page path to test.
+   * @param $expected_tags
+   *   The expected cache tags for the page cache entry of the given $path.
+   */
+  protected function verifyPageCacheTags($path, $expected_tags) {
+    $this->drupalGet($path);
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+    $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, $expected_tags);
+  }
+
+}
