diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index 88495be34041986ebcc1654d54e7875b06f5c382..a672e4ee240e446cf1633135bb0997a82edb218a 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -14,12 +14,20 @@ * Implements hook_toolbar(). */ function contextual_toolbar() { + $items = []; + $items['contextual'] = [ + '#cache' => [ + 'contexts' => [ + 'user.permissions', + ], + ], + ]; if (!\Drupal::currentUser()->hasPermission('access contextual links')) { - return; + return $items; } - $tab['contextual'] = array( + $items['contextual'] += array( '#type' => 'toolbar_item', 'tab' => array( '#type' => 'html_tag', @@ -41,7 +49,7 @@ function contextual_toolbar() { ), ); - return $tab; + return $items; } /** diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index ff328916c94f32c53ab7a84df010724c8290ad3c..ccca313101d96bb8221e53c65e147ec324081ccf 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -364,10 +364,22 @@ function shortcut_preprocess_page(&$variables) { function shortcut_toolbar() { $user = \Drupal::currentUser(); - $items = array(); + $items = []; + $items['shortcuts'] = [ + '#cache' => [ + 'contexts' => [ + // Cacheable per user, because each user can have their own shortcut + // set, even if they cannot create or select a shortcut set, because + // an administrator may have assigned a non-default shortcut set. + 'user', + ], + ], + ]; + if ($user->hasPermission('access shortcuts')) { $links = shortcut_renderable_links(); $shortcut_set = shortcut_current_displayed_set(); + \Drupal::service('renderer')->addCacheableDependency($items['shortcuts'], $shortcut_set); $configure_link = NULL; if (shortcut_set_edit_access($shortcut_set)->isAllowed()) { $configure_link = array( @@ -378,7 +390,7 @@ function shortcut_toolbar() { ); } if (!empty($links) || !empty($configure_link)) { - $items['shortcuts'] = array( + $items['shortcuts'] += array( '#type' => 'toolbar_item', 'tab' => array( '#type' => 'link', diff --git a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php index bc6c43aecc797ae37b405f74807ac16f6d8e6a64..4893c547425a927cb957cf47f86b1b21a85e3b53 100644 --- a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php +++ b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php @@ -102,16 +102,22 @@ protected function assertCacheTags(array $expected_tags) { * * @param string[] $expected_contexts * The expected cache contexts. + * @param string $message + * (optional) A verbose message to output. + * + * @return + * TRUE if the assertion succeeded, FALSE otherwise. */ - protected function assertCacheContexts(array $expected_contexts) { + protected function assertCacheContexts(array $expected_contexts, $message = NULL) { $actual_contexts = $this->getCacheHeaderValues('X-Drupal-Cache-Contexts'); sort($expected_contexts); sort($actual_contexts); - $this->assertIdentical($actual_contexts, $expected_contexts); - if ($actual_contexts !== $expected_contexts) { + $return = $this->assertIdentical($actual_contexts, $expected_contexts, $message); + if (!$return) { debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts))); debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts))); } + return $return; } /** diff --git a/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php b/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fb8c14c7852290bae8e4264757843bf0b4211ff4 --- /dev/null +++ b/core/modules/toolbar/src/Tests/ToolbarCacheContextsTest.php @@ -0,0 +1,150 @@ +<?php + +/** + * @file + * Contains \Drupal\toolbar\Tests\ToolbarCacheContextsTest. + */ + +namespace Drupal\toolbar\Tests; + +use Drupal\Core\Cache\Cache; +use Drupal\simpletest\WebTestBase; +use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait; + + +/** + * Tests the cache contexts for toolbar. + * + * @group toolbar + */ +class ToolbarCacheContextsTest extends WebTestBase { + + use AssertPageCacheContextsAndTagsTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['toolbar', 'test_page_test']; + + /** + * An authenticated user to use for testing. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * An authenticated user to use for testing. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser2; + + /** + * A list of default permissions for test users. + * + * @var array + */ + protected $perms = [ + 'access toolbar', + 'access administration pages', + 'administer site configuration', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->adminUser = $this->drupalCreateUser($this->perms); + $this->adminUser2 = $this->drupalCreateUser($this->perms); + } + + /** + * Tests toolbar cache contexts. + */ + public function testToolbarCacheContextsCaller() { + // Test with default combination and permission to see toolbar. + $this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found for default combination and permission to see toolbar.'); + + // Test without user toolbar tab. User module is a required module so we have to + // manually remove the user toolbar tab. + $this->installModules(['toolbar_disable_user_toolbar']); + $this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found without user toolbar tab.'); + + // Test with the toolbar and contextual enabled. + $this->installModules(['contextual']); + $this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access contextual links'])); + $this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with contextual module enabled.'); + \Drupal::service('module_installer')->uninstall(['contextual']); + + // Test with the tour module enabled. + $this->installModules(['tour']); + $this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access tour'])); + $this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with tour module enabled.'); + \Drupal::service('module_installer')->uninstall(['tour']); + + // Test with shortcut module enabled. + $this->installModules(['shortcut']); + $this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access shortcuts', 'administer shortcuts'])); + $this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found with shortcut module enabled.'); + } + + /** + * Tests that cache contexts are applied for both users. + * + * @param string[] $cache_contexts + * Expected cache contexts for both users. + * @param string $message + * (optional) A verbose message to output. + * + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertToolbarCacheContexts(array $cache_contexts, $message = NULL) { + // Default cache contexts that should exist on all test cases. + $default_cache_contexts = [ + 'languages:language_interface', + 'theme', + ]; + $cache_contexts = Cache::mergeContexts($default_cache_contexts, $cache_contexts); + + // Assert contexts for user1 which has only default permissions. + $this->drupalLogin($this->adminUser); + $this->drupalGet('test-page'); + $return = $this->assertCacheContexts($cache_contexts); + $this->drupalLogout(); + + // Assert contexts for user2 which has some additional permissions. + $this->drupalLogin($this->adminUser2); + $this->drupalGet('test-page'); + $return = $return && $this->assertCacheContexts($cache_contexts); + + if ($return) { + $this->pass($message); + } + else { + $this->fail($message); + } + return $return; + } + + /** + * Installs a given list of modules and rebuilds the cache. + * + * @param string[] $module_list + * An array of module names. + */ + protected function installModules(array $module_list) { + \Drupal::service('module_installer')->install($module_list); + + // Installing modules updates the container and needs a router rebuild. + $this->container = \Drupal::getContainer(); + $this->container->get('router.builder')->rebuildIfNeeded(); + } + + +} diff --git a/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.info.yml b/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..e97d0ea9d1a69f930ad737b0407eca1c0f8530e6 --- /dev/null +++ b/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.info.yml @@ -0,0 +1,6 @@ +name: 'Disable user toolbar' +type: module +description: 'Support module for testing toolbar without user toolbar' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.module b/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.module new file mode 100644 index 0000000000000000000000000000000000000000..067899861d21fbb4dee44993ebc56969f545e96d --- /dev/null +++ b/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.module @@ -0,0 +1,8 @@ +<?php + +/** + * Implements hook_toolbar_alter(). + */ +function toolbar_disable_user_toolbar_toolbar_alter(&$items) { + unset($items['user']); +} diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 20665708f89f85082d744d9fd531337ef49063fa..256b4e35fffdb8d65c34bfc67e4d6fb5ca0ddb30 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -59,6 +59,10 @@ function toolbar_page_top(array &$page_top) { $page_top['toolbar'] = array( '#type' => 'toolbar', '#access' => \Drupal::currentUser()->hasPermission('access toolbar'), + '#cache' => [ + 'keys' => ['toolbar'], + 'contexts' => ['user.permissions'], + ], ); } @@ -97,17 +101,19 @@ function template_preprocess_toolbar(&$variables) { } } - $attributes = array(); - // Pass the wrapper attributes along. - if (array_key_exists('#wrapper_attributes', $element[$key])) { - $attributes = $element[$key]['#wrapper_attributes']; - } - // Add the tab. - $variables['tabs'][$key] = array( - 'link' => $element[$key]['tab'], - 'attributes' => new Attribute($attributes), - ); + if (isset($element[$key]['tray'])) { + $attributes = array(); + // Pass the wrapper attributes along. + if (array_key_exists('#wrapper_attributes', $element[$key])) { + $attributes = $element[$key]['#wrapper_attributes']; + } + + $variables['tabs'][$key] = array( + 'link' => $element[$key]['tab'], + 'attributes' => new Attribute($attributes), + ); + } // Add other non-tray, non-tab child elements to the remainder variable for // later rendering. diff --git a/core/modules/tour/tour.module b/core/modules/tour/tour.module index f4846afed7da0255cc2f23b3b2d61f90cd411a0d..a5e44148dec4b1af521c952d6e737085ac865f66 100644 --- a/core/modules/tour/tour.module +++ b/core/modules/tour/tour.module @@ -32,11 +32,20 @@ function tour_help($route_name, RouteMatchInterface $route_match) { * Implements hook_toolbar(). */ function tour_toolbar() { + $items = []; + $items['tour'] = [ + '#cache' => [ + 'contexts' => [ + 'user.permissions', + ], + ], + ]; + if (!\Drupal::currentUser()->hasPermission('access tour')) { - return; + return $items; } - $tab['tour'] = array( + $items['tour'] += array( '#type' => 'toolbar_item', 'tab' => array( '#type' => 'html_tag', @@ -59,7 +68,7 @@ function tour_toolbar() { ), ); - return $tab; + return $items; } /** diff --git a/core/modules/user/user.module b/core/modules/user/user.module index db3cfb1cfd26bc9e1b485f8f61ff070bb3a4b3aa..1e6de88c236d431932431bddebd077db1c105350 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -4,6 +4,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Unicode; use Drupal\Core\Asset\AttachedAssetsInterface; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Form\FormStateInterface; @@ -1317,6 +1318,7 @@ function user_toolbar() { $user = \Drupal::currentUser(); // Add logout & user account links or login link. + $links_cache_contexts = []; if ($user->isAuthenticated()) { $links = array( 'account' => array( @@ -1338,6 +1340,8 @@ function user_toolbar() { 'url' => Url::fromRoute('user.logout'), ), ); + // The "Edit user account" link is per-user. + $links_cache_contexts[] = 'user'; } else { $links = array( @@ -1358,10 +1362,21 @@ function user_toolbar() { 'title' => t('My account'), 'class' => array('toolbar-icon', 'toolbar-icon-user'), ), + '#cache' => [ + 'contexts' => [ + // Cacheable per user, because the current user's name is shown. + 'user', + ], + ], ), 'tray' => array( '#heading' => t('User account actions'), 'user_links' => array( + '#cache' => [ + // Cacheable per "authenticated or not", because the links to + // display depend on that. + 'contexts' => Cache::mergeContexts(array('user.roles:authenticated'), $links_cache_contexts), + ], '#theme' => 'links__toolbar_user', '#links' => $links, '#attributes' => array(