 core/modules/book/lib/Drupal/book/BookManager.php  |   29 +++++---
 .../book/lib/Drupal/book/BookManagerInterface.php  |   13 ++++
 .../book/Plugin/Block/BookNavigationBlock.php      |   50 +++++++++++---
 .../menu_link/lib/Drupal/menu_link/MenuTree.php    |   72 +++++++++++++-------
 .../lib/Drupal/menu_link/MenuTreeInterface.php     |   11 +++
 .../Drupal/system/Plugin/Block/SystemMenuBlock.php |   18 +++--
 6 files changed, 142 insertions(+), 51 deletions(-)

diff --git a/core/modules/book/lib/Drupal/book/BookManager.php b/core/modules/book/lib/Drupal/book/BookManager.php
index e17f9e7..e1f0790 100644
--- a/core/modules/book/lib/Drupal/book/BookManager.php
+++ b/core/modules/book/lib/Drupal/book/BookManager.php
@@ -468,16 +468,9 @@ public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL) {
         'max_depth' => $max_depth,
       );
       if ($nid) {
-        // The tree is for a single item, so we need to match the values in its
-        // p columns and 0 (the top level) with the plid values of other links.
-        $parents = array(0);
-        for ($i = 1; $i < static::BOOK_MAX_DEPTH; $i++) {
-          if (!empty($link["p$i"])) {
-            $parents[] = $link["p$i"];
-          }
-        }
-        $tree_parameters['expanded'] = $parents;
-        $tree_parameters['active_trail'] = $parents;
+        $active_trail = $this->getActiveTrailIds($bid, $link);
+        $tree_parameters['expanded'] = $active_trail;
+        $tree_parameters['active_trail'] = $active_trail;
         $tree_parameters['active_trail'][] = $nid;
       }
 
@@ -491,6 +484,22 @@ public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL) {
   /**
    * {@inheritdoc}
    */
+  public function getActiveTrailIds($bid, $link) {
+    $nid = isset($link['nid']) ? $link['nid'] : 0;
+    // The tree is for a single item, so we need to match the values in its
+    // p columns and 0 (the top level) with the plid values of other links.
+    $active_trail = array(0);
+    for ($i = 1; $i < static::BOOK_MAX_DEPTH; $i++) {
+      if (!empty($link["p$i"])) {
+        $active_trail[] = $link["p$i"];
+      }
+    }
+    return $active_trail;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function bookTreeOutput(array $tree) {
     $build = array();
     $items = array();
diff --git a/core/modules/book/lib/Drupal/book/BookManagerInterface.php b/core/modules/book/lib/Drupal/book/BookManagerInterface.php
index 18725a9..5dae027 100644
--- a/core/modules/book/lib/Drupal/book/BookManagerInterface.php
+++ b/core/modules/book/lib/Drupal/book/BookManagerInterface.php
@@ -41,6 +41,19 @@
   public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL);
 
   /**
+   * Gets the active trail IDs for the specified book at the provided path.
+   *
+   * @param string $bid
+   *   The Book ID to find links for.
+   * @param array $link
+   *   A fully loaded menu link.
+   *
+   * @return array
+   *   An array containing the active trail: a list of mlids.
+   */
+  public function getActiveTrailIds($bid, $link);
+
+  /**
    * Loads a single book entry.
    *
    * @param int $nid
diff --git a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
index 6a9cfff..06cd262 100644
--- a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
+++ b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
@@ -8,6 +8,7 @@
 namespace Drupal\book\Plugin\Block;
 
 use Drupal\block\BlockBase;
+use Drupal\book\BookManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -31,6 +32,13 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt
   protected $request;
 
   /**
+   * The book manager.
+   *
+   * @var \Drupal\book\BookManagerInterface
+   */
+  protected $bookManager;
+
+  /**
    * Constructs a new BookNavigationBlock instance.
    *
    * @param array $configuration
@@ -41,11 +49,14 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt
    *   The plugin implementation definition.
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request object.
+   * @param \Drupal\book\BookManagerInterface $book_manager
+   *   The book manager.
    */
-  public function __construct(array $configuration, $plugin_id, array $plugin_definition, Request $request) {
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, Request $request, BookManagerInterface $book_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->request = $request;
+    $this->bookManager = $book_manager;
   }
 
   /**
@@ -56,7 +67,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('request')
+      $container->get('request'),
+      $container->get('book.manager')
     );
   }
 
@@ -111,8 +123,8 @@ public function build() {
         if ($book['bid'] == $current_bid) {
           // If the current page is a node associated with a book, the menu
           // needs to be retrieved.
-          $data = \Drupal::service('book.manager')->bookTreeAllData($node->book['bid'], $node->book);
-          $book_menus[$book_id] = \Drupal::service('book.manager')->bookTreeOutput($data);
+          $data = $this->bookManager->bookTreeAllData($node->book['bid'], $node->book);
+          $book_menus[$book_id] = $this->bookManager->bookTreeOutput($data);
         }
         else {
           // Since we know we will only display a link to the top node, there
@@ -122,7 +134,7 @@ public function build() {
           $book_node = node_load($book['nid']);
           $book['access'] = $book_node->access('view');
           $pseudo_tree[0]['link'] = $book;
-          $book_menus[$book_id] = \Drupal::service('book.manager')->bookTreeOutput($pseudo_tree);
+          $book_menus[$book_id] = $this->bookManager->bookTreeOutput($pseudo_tree);
         }
       }
       if ($book_menus) {
@@ -140,10 +152,10 @@ public function build() {
       $nid = $select->execute()->fetchField();
       // Only show the block if the user has view access for the top-level node.
       if ($nid) {
-        $tree = \Drupal::service('book.manager')->bookTreeAllData($node->book['bid'], $node->book);
+        $tree = $this->bookManager->bookTreeAllData($node->book['bid'], $node->book);
         // There should only be one element at the top level.
         $data = array_shift($tree);
-        $below = \Drupal::service('book.manager')->bookTreeOutput($data['below']);
+        $below = $this->bookManager->bookTreeOutput($data['below']);
         if (!empty($below)) {
           return $below;
         }
@@ -155,11 +167,27 @@ public function build() {
   /**
    * {@inheritdoc}
    */
+  public function getCacheKeys() {
+    // Add a key for the active book trail.
+    $current_bid = 0;
+    if ($node = $this->request->get('node')) {
+      $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
+    }
+    if ($current_bid === 0) {
+      return parent::getCacheKeys();
+    }
+    $active_trail = $this->bookManager->getActiveTrailIds($node->book['bid'], $node->book);
+    $active_trail_key = 'trail.' . implode('|', $active_trail);
+    return array_merge(parent::getCacheKeys(), array($active_trail_key));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   protected function getRequiredCacheContexts() {
-    // The "Book navigation" block must be cached per URL and per role: the
-    // "active" menu link may differ per URL and different roles may have access
-    // to different menu links.
-    return array('cache_context.url', 'cache_context.user.roles');
+    // The "Book navigation" block must be cached per role: different roles may
+    // have access to different menu links.
+    return array('cache_context.user.roles');
   }
 
 }
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
index ecf756a..bacb156 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php
@@ -204,8 +204,6 @@ public function buildAllData($menu_name, $link = NULL, $max_depth = NULL) {
   public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
     $language_interface = $this->languageManager->getCurrentLanguage();
 
-    // Check if the active trail has been overridden for this menu tree.
-    $active_path = $this->getPath($menu_name);
     // Load the request corresponding to the current page.
     $request = $this->requestStack->getCurrentRequest();
     $system_path = NULL;
@@ -247,34 +245,17 @@ public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail
             'min_depth' => 1,
             'max_depth' => $max_depth,
           );
-          // Parent mlids; used both as key and value to ensure uniqueness.
-          // We always want all the top-level links with plid == 0.
-          $active_trail = array(0 => 0);
+          $active_trail = $this->getActiveTrailIds($menu_name);
 
           // If this page is accessible to the current user, build the tree
           // parameters accordingly.
           if ($page_not_403) {
-            // Find a menu link corresponding to the current path. If
-            // $active_path is NULL, let menu_link_get_preferred() determine
-            // the path.
-            if ($active_link = $this->menuLinkGetPreferred($menu_name, $active_path)) {
-              // The active link may only be taken into account to build the
-              // active trail, if it resides in the requested menu.
-              // Otherwise, we'd needlessly re-run _menu_build_tree() queries
-              // for every menu on every page.
-              if ($active_link['menu_name'] == $menu_name) {
-                // Use all the coordinates, except the last one because
-                // there can be no child beyond the last column.
-                for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
-                  if ($active_link['p' . $i]) {
-                    $active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
-                  }
-                }
-                // If we are asked to build links for the active trail only,skip
-                // the entire 'expanded' handling.
-                if ($only_active_trail) {
-                  $tree_parameters['only_active_trail'] = TRUE;
-                }
+            // The active trail contains more than only array(0 => 0).
+            if (count($active_trail) > 1) {
+              // If we are asked to build links for the active trail only,skip
+              // the entire 'expanded' handling.
+              if ($only_active_trail) {
+                $tree_parameters['only_active_trail'] = TRUE;
               }
             }
             $parents = $active_trail;
@@ -320,6 +301,45 @@ public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail
   /**
    * {@inheritdoc}
    */
+  public function getActiveTrailIds($menu_name) {
+    $request = $this->requestStack->getCurrentRequest();
+    $system_path = NULL;
+    if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) {
+      // @todo https://drupal.org/node/2068471 is adding support so we can tell
+      // if this is called on a 404/403 page.
+      $system_path = $request->attributes->get('_system_path');
+      $page_not_403 = 1;
+    }
+
+    // Parent mlids; used both as key and value to ensure uniqueness.
+    // We always want all the top-level links with plid == 0.
+    $active_trail = array(0 => 0);
+    // If this page is accessible to the current user, build the tree
+    // parameters accordingly.
+    if ($page_not_403) {
+      // Check if the active trail has been overridden for this menu tree.
+      $active_path = $this->getPath($menu_name);
+      // Find a menu link corresponding to the current path. If
+      // $active_path is NULL, let menu_link_get_preferred() determine
+      // the path.
+      if ($active_link = $this->menuLinkGetPreferred($menu_name, $active_path)) {
+        if ($active_link['menu_name'] == $menu_name) {
+          // Use all the coordinates, except the last one because
+          // there can be no child beyond the last column.
+          for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
+            if ($active_link['p' . $i]) {
+              $active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
+            }
+          }
+        }
+      }
+    }
+    return $active_trail;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function setPath($menu_name, $path = NULL) {
     if (isset($path)) {
       $this->trailPaths[$menu_name] = $path;
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
index e4cd83d..6cb192f 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php
@@ -60,6 +60,17 @@ public function setPath($menu_name, $path = NULL);
   public function getPath($menu_name);
 
   /**
+   * Gets the active trail IDs of the specified menu tree.
+   *
+   * @param string $menu_name
+   *   The menu name of the requested tree.
+   *
+   * @return array
+   *   An array containing the active trail: a list of mlids.
+   */
+  public function getActiveTrailIds($menu_name);
+
+  /**
    * Sorts and returns the built data representing a menu tree.
    *
    * @param array $links
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
index 32a84d7..14c0837 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
@@ -88,6 +88,17 @@ public function defaultConfiguration() {
   /**
    * {@inheritdoc}
    */
+  public function getCacheKeys() {
+    // Add a key for the active menu trail.
+    $menu = $this->getDerivativeId();
+    $active_trail = $this->menuTree->getActiveTrailIds($menu);
+    $active_trail_key = 'trail.' . implode('|', $active_trail);
+    return array_merge(parent::getCacheKeys(), array($active_trail_key));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getCacheTags() {
     // Even when the menu block renders to the empty string for a user, we want
     // the cache tag for this menu to be set: whenever the menu is changed, this
@@ -101,10 +112,9 @@ public function getCacheTags() {
    * {@inheritdoc}
    */
   protected function getRequiredCacheContexts() {
-    // Menu blocks must be cached per URL and per role: the "active" menu link
-    // may differ per URL and different roles may have access to different menu
-    // links.
-    return array('cache_context.url', 'cache_context.user.roles');
+    // Menu blocks must be cached per role: different roles may have access to
+    // different menu links.
+    return array('cache_context.user.roles');
   }
 
 }
