 core/lib/Drupal/Core/Session/AccountInterface.php  |   10 +++
 core/lib/Drupal/Core/Session/UserSession.php       |   20 ++++++
 .../Drupal/block/Tests/Views/DisplayBlockTest.php  |    2 +-
 core/modules/contextual/contextual.module          |    3 -
 core/modules/contextual/js/contextual.js           |   75 ++++++++++++++------
 .../Tests/ContextualDynamicContextTest.php         |    4 +-
 core/modules/edit/js/edit.js                       |   63 +++++++++++++---
 .../menu/lib/Drupal/menu/Tests/MenuTest.php        |    2 +-
 core/modules/user/lib/Drupal/user/Entity/Role.php  |   14 +++-
 core/modules/user/lib/Drupal/user/Entity/User.php  |   21 ++++++
 .../user/lib/Drupal/user/RoleStorageController.php |    1 +
 .../lib/Drupal/user/Tests/UserPermissionsTest.php  |   15 ++++
 core/modules/user/user.module                      |    6 +-
 .../lib/Drupal/views_ui/Tests/DisplayTest.php      |    2 +-
 14 files changed, 198 insertions(+), 40 deletions(-)

diff --git a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
index 4a1e64d..8133331 100644
--- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
@@ -240,7 +240,7 @@ public function testBlockContextualLinks() {
     $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-uiedit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="views-uiedit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
   }

 }
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index e405f20..f13b639 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -283,9 +283,6 @@ function contextual_pre_render_links($element) {
       'route_name' => isset($item['route_name']) ? $item['route_name'] : '',
       'route_parameters' => isset($item['route_parameters']) ? $item['route_parameters'] : array(),
     );
-    $item['localized_options'] += array('query' => array());
-    $item['localized_options']['query'] += drupal_get_destination();
-    $links[$class] += $item['localized_options'];
   }
   $element['#links'] = $links;

diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js
index 0ea3815..69272a7 100644
--- a/core/modules/contextual/js/contextual.js
+++ b/core/modules/contextual/js/contextual.js
@@ -3,7 +3,7 @@
  * Attaches behaviors for the Contextual module.
  */

-(function ($, Drupal, drupalSettings, Backbone, Modernizr) {
+(function ($, Drupal, drupalSettings, _, Backbone, Modernizr, JSON, storage) {

 "use strict";

@@ -17,24 +17,51 @@ var options = $.extend(drupalSettings.contextual,
   }
 );

+// Clear the cached contextual links whenever the current user's set of
+// permissions changes.
+var cachedPermissionsHash = storage.getItem('Drupal.contextual.permissionsHash');
+var permissionsHash = drupalSettings.user.permissionsHash;
+if (cachedPermissionsHash !== permissionsHash) {
+  if (typeof permissionsHash === 'string') {
+    _.chain(storage).keys().each(function (key) {
+      if (key.substring(0, 18) === 'Drupal.contextual.') {
+        storage.removeItem(key);
+      }
+    });
+  }
+  storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
+}
+
 /**
  * Initializes a contextual link: updates its DOM, sets up model and views
  *
  * @param jQuery $contextual
  *   A contextual links placeholder DOM element, containing the actual
  *   contextual links as rendered by the server.
+ * @param string html
+ *   The server-side rendered HTML for this contextual link.
  */
-function initContextual ($contextual) {
+function initContextual ($contextual, html) {
   var $region = $contextual.closest('.contextual-region');
   var contextual = Drupal.contextual;

   $contextual
+    // Update the placeholder to contain its rendered contextual links.
+    .html(html)
     // Use the placeholder as a wrapper with a specific class to provide
     // positioning and behavior attachment context.
     .addClass('contextual')
     // Ensure a trigger element exists before the actual contextual links.
     .prepend(Drupal.theme('contextualTrigger'));

+  // Set the destination parameter on each of the contextual links.
+  var destination = 'destination=' + Drupal.encodePath(drupalSettings.currentPath);
+  $contextual.find('.contextual-links a').each(function () {
+    var url = this.getAttribute('href');
+    var glue = (url.indexOf('?') === -1) ? '?' : '&';
+    this.setAttribute('href', url + glue + destination);
+  });
+
   // Create a model and the appropriate views.
   var model = new contextual.Model({
     title: $region.find('h2:first').text().trim()
@@ -128,26 +155,34 @@ Drupal.behaviors.contextual = {
       ids.push($(this).attr('data-contextual-id'));
     });

-    // Perform an AJAX request to let the server render the contextual links for
-    // each of the placeholders.
-    $.ajax({
-      url: Drupal.url('contextual/render') + '?destination=' + Drupal.encodePath(drupalSettings.currentPath),
-      type: 'POST',
-      data: { 'ids[]' : ids },
-      dataType: 'json',
-      success: function (results) {
-        for (var id in results) {
-          if (results.hasOwnProperty(id)) {
-            // Update the placeholder to contain its rendered contextual links.
-            var $placeholder = $context.find('[data-contextual-id="' + id + '"]')
-              .html(results[id]);
+    // Update all contextual links placeholders whose HTML is cached.
+    var uncachedIDs = _.filter(ids, function initIfCached (contextualID) {
+      var html = storage.getItem('Drupal.contextual.' + contextualID);
+      if (html !== null) {
+        initContextual($context.find('[data-contextual-id="' + contextualID + '"]'), html);
+        return false;
+      }
+      return true;
+    });

+    // Perform an AJAX request to let the server render the contextual links for
+    // each of the placeholders whose HTML is not yet cached.
+    if (uncachedIDs.length > 0) {
+      $.ajax({
+        url: Drupal.url('contextual/render'),
+        type: 'POST',
+        data: { 'ids[]' : uncachedIDs },
+        dataType: 'json',
+        success: function (results) {
+          _.each(results, function (html, contextualID) {
+            // Store the metadata.
+            storage.setItem('Drupal.contextual.' + contextualID, html);
             // Initialize the contextual link.
-            initContextual($placeholder);
-          }
+            initContextual($context.find('[data-contextual-id="' + contextualID + '"]'), html);
+          });
         }
-      }
-     });
+      });
+    }
   }
 };

@@ -417,4 +452,4 @@ Drupal.theme.contextualTrigger = function () {
   return '<button class="trigger visually-hidden focusable" type="button"></button>';
 };

-})(jQuery, Drupal, drupalSettings, Backbone, Modernizr);
+})(jQuery, Drupal, drupalSettings, _, Backbone, Modernizr, window.JSON, window.sessionStorage);
diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
index 4cbbca3..bdca035 100644
--- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
+++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
@@ -77,9 +77,9 @@ function testDifferentPermissions() {
     $response = $this->renderContextualLinks($ids, 'node');
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="nodepage-edit odd first last"><a href="' . base_path() . 'node/1/edit?destination=node">Edit</a></li></ul>');
+    $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="nodepage-edit odd first last"><a href="' . base_path() . 'node/1/edit">Edit</a></li></ul>');
     $this->assertIdentical($json[$ids[1]], '');
-    $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="nodepage-edit odd first last"><a href="' . base_path() . 'node/3/edit?destination=node">Edit</a></li></ul>');
+    $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="nodepage-edit odd first last"><a href="' . base_path() . 'node/3/edit">Edit</a></li></ul>');
     $this->assertIdentical($json[$ids[3]], '');

     // Authenticated user: can access contextual links, cannot edit articles.
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index f4f9e53..48baaa1 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
@@ -418,7 +418,7 @@ public function testBlockContextualLinks() {
     $response =  $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="menu-edit even last"><a href="' . base_path() . 'admin/structure/menu/manage/tools?destination=test-page">Edit menu</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="menu-edit even last"><a href="' . base_path() . 'admin/structure/menu/manage/tools">Edit menu</a></li></ul>');
   }

   /**
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
index 905095a..f8682c3 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
@@ -298,7 +298,7 @@ public function testPageContextualLinks() {
     $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-uiedit odd first last"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1?destination=test-display">Edit view</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-uiedit odd first last"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1">Edit view</a></li></ul>');
   }

   /**
