diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index f6da17897e34ae3620fb7ad60cd8f3c2351cfc40..bbc1d8c120f17682623e4d8c707bdf781eefc8f8 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -8,6 +8,7 @@ use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\shortcut\Entity\ShortcutSet; @@ -306,10 +307,10 @@ function shortcut_preprocess_block(&$variables) { */ function shortcut_preprocess_page_title(&$variables) { // Only display the shortcut link if the user has the ability to edit - // shortcuts and if the page's actual content is being shown (for example, - // we do not want to display it on "access denied" or "page not found" - // pages). - if (shortcut_set_edit_access()->isAllowed() && !\Drupal::request()->attributes->has('exception')) { + // shortcuts, the feature is enabled for the current theme and if the page's + // actual content is being shown (for example, we do not want to display it on + // "access denied" or "page not found" pages). + if (shortcut_set_edit_access()->isAllowed() && theme_get_setting('third_party_settings.shortcut.module_link') && !\Drupal::request()->attributes->has('exception')) { $link = Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath(); $route_match = \Drupal::routeMatch(); @@ -324,6 +325,12 @@ function shortcut_preprocess_page_title(&$variables) { $shortcut_set = shortcut_current_displayed_set(); + // Pages with the add or remove shortcut button need cache invalidation when + // a shortcut is added, edited, or removed. + $cacheability_metadata = CacheableMetadata::createFromRenderArray($variables); + $cacheability_metadata->addCacheTags(\Drupal::entityTypeManager()->getDefinition('shortcut')->getListCacheTags()); + $cacheability_metadata->applyTo($variables); + // Check if $link is already a shortcut and set $link_mode accordingly. $shortcuts = \Drupal::entityTypeManager()->getStorage('shortcut')->loadByProperties(['shortcut_set' => $shortcut_set->id()]); /** @var \Drupal\shortcut\ShortcutInterface $shortcut */ @@ -347,26 +354,24 @@ function shortcut_preprocess_page_title(&$variables) { $route_parameters = ['shortcut' => $shortcut_id]; } - if (theme_get_setting('third_party_settings.shortcut.module_link')) { - $query += \Drupal::destination()->getAsArray(); - $variables['title_suffix']['add_or_remove_shortcut'] = [ - '#attached' => [ - 'library' => [ - 'shortcut/drupal.shortcut', - ], + $query += \Drupal::destination()->getAsArray(); + $variables['title_suffix']['add_or_remove_shortcut'] = [ + '#attached' => [ + 'library' => [ + 'shortcut/drupal.shortcut', ], - '#type' => 'link', - '#title' => new FormattableMarkup('<span class="shortcut-action__icon"></span><span class="shortcut-action__message">@text</span>', ['@text' => $link_text]), - '#url' => Url::fromRoute($route_name, $route_parameters), - '#options' => ['query' => $query], - '#attributes' => [ - 'class' => [ - 'shortcut-action', - 'shortcut-action--' . $link_mode, - ], + ], + '#type' => 'link', + '#title' => new FormattableMarkup('<span class="shortcut-action__icon"></span><span class="shortcut-action__message">@text</span>', ['@text' => $link_text]), + '#url' => Url::fromRoute($route_name, $route_parameters), + '#options' => ['query' => $query], + '#attributes' => [ + 'class' => [ + 'shortcut-action', + 'shortcut-action--' . $link_mode, ], - ]; - } + ], + ]; } } @@ -380,52 +385,43 @@ function shortcut_toolbar() { $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', + 'user.permissions', ], ], ]; 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 = [ + + $items['shortcuts'] += [ + '#type' => 'toolbar_item', + 'tab' => [ '#type' => 'link', - '#title' => t('Edit shortcuts'), - '#url' => Url::fromRoute('entity.shortcut_set.customize_form', ['shortcut_set' => $shortcut_set->id()]), - '#options' => ['attributes' => ['class' => ['edit-shortcuts']]], - ]; - } - if (!empty($links) || !empty($configure_link)) { - $items['shortcuts'] += [ - '#type' => 'toolbar_item', - 'tab' => [ - '#type' => 'link', - '#title' => t('Shortcuts'), - '#url' => $shortcut_set->toUrl('collection'), - '#attributes' => [ - 'title' => t('Shortcuts'), - 'class' => ['toolbar-icon', 'toolbar-icon-shortcut'], - ], - ], - 'tray' => [ - '#heading' => t('User-defined shortcuts'), - 'shortcuts' => $links, - 'configure' => $configure_link, + '#title' => t('Shortcuts'), + '#url' => $shortcut_set->toUrl('collection'), + '#attributes' => [ + 'title' => t('Shortcuts'), + 'class' => ['toolbar-icon', 'toolbar-icon-shortcut'], ], - '#weight' => -10, - '#attached' => [ - 'library' => [ - 'shortcut/drupal.shortcut', + ], + 'tray' => [ + '#heading' => t('User-defined shortcuts'), + 'children' => [ + '#lazy_builder' => ['shortcut.lazy_builders:lazyLinks', []], + '#create_placeholder' => TRUE, + '#cache' => [ + 'keys' => ['shortcut_set_toolbar_links'], + 'contexts' => ['user'], ], ], - ]; - } + ], + '#weight' => -10, + '#attached' => [ + 'library' => [ + 'shortcut/drupal.shortcut', + ], + ], + ]; } return $items; diff --git a/core/modules/shortcut/shortcut.services.yml b/core/modules/shortcut/shortcut.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..49244aa440ef06e549ac177ad1e0fcb0491d7ac9 --- /dev/null +++ b/core/modules/shortcut/shortcut.services.yml @@ -0,0 +1,4 @@ +services: + shortcut.lazy_builders: + class: Drupal\shortcut\ShortcutLazyBuilders + arguments: ['@renderer'] diff --git a/core/modules/shortcut/src/ShortcutLazyBuilders.php b/core/modules/shortcut/src/ShortcutLazyBuilders.php new file mode 100644 index 0000000000000000000000000000000000000000..e2672104c59fdfdde11d1a09e5523a3744740efa --- /dev/null +++ b/core/modules/shortcut/src/ShortcutLazyBuilders.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\shortcut; + +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Security\TrustedCallbackInterface; +use Drupal\Core\Url; + +/** + * Lazy builders for the shortcut module. + */ +class ShortcutLazyBuilders implements TrustedCallbackInterface { + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs a new ShortcutLazyBuilders object. + * + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + */ + public function __construct(RendererInterface $renderer) { + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['lazyLinks']; + } + + /** + * #lazy_builder callback; builds shortcut toolbar links. + * + * @return array + * A renderable array of shortcut links. + */ + public function lazyLinks() { + $shortcut_set = shortcut_current_displayed_set(); + + $links = shortcut_renderable_links(); + + $configure_link = NULL; + if (shortcut_set_edit_access($shortcut_set)->isAllowed()) { + $configure_link = [ + '#type' => 'link', + '#title' => t('Edit shortcuts'), + '#url' => Url::fromRoute('entity.shortcut_set.customize_form', ['shortcut_set' => $shortcut_set->id()]), + '#options' => ['attributes' => ['class' => ['edit-shortcuts']]], + ]; + } + + $build = [ + 'shortcuts' => $links, + 'configure' => $configure_link, + ]; + $this->renderer->addCacheableDependency($build, $shortcut_set); + + return $build; + } + +} diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php index 71720d4442e4b782bfa4ba54ae20a7e09dfb6cfb..52876a4e37ff71a370385165b1f1e9d21be505b8 100644 --- a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php @@ -3,7 +3,9 @@ namespace Drupal\Tests\shortcut\Functional; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Url; use Drupal\shortcut\Entity\Shortcut; +use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; use Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; @@ -14,11 +16,12 @@ * @group shortcut */ class ShortcutCacheTagsTest extends EntityCacheTagsTestBase { + use AssertPageCacheContextsAndTagsTrait; /** * {@inheritdoc} */ - public static $modules = ['shortcut']; + public static $modules = ['toolbar', 'shortcut', 'test_page_test', 'block']; /** * {@inheritdoc} @@ -73,4 +76,121 @@ public function testEntityCreation() { $this->assertFalse(\Drupal::cache('render')->get('foo'), 'Creating a new shortcut invalidates the cache tag of the shortcut set.'); } + /** + * Tests visibility and cacheability of shortcuts in the toolbar. + */ + public function testToolbar() { + $this->drupalPlaceBlock('page_title_block', ['id' => 'title']); + + $test_page_url = Url::fromRoute('test_page_test.test_page'); + $this->verifyPageCache($test_page_url, 'MISS'); + $this->verifyPageCache($test_page_url, 'HIT'); + + // Ensure that without enabling the shortcuts-in-page-title-link feature + // in the theme, the shortcut_list cache tag is not added to the page. + $this->drupalLogin($this->rootUser); + $this->drupalGet('admin/config/system/cron'); + $expected_cache_tags = [ + 'block_view', + 'config:block.block.title', + 'config:block_list', + 'config:shortcut.set.default', + 'config:system.menu.admin', + 'config:user.role.authenticated', + 'rendered', + 'user:' . $this->rootUser->id(), + ]; + $this->assertCacheTags($expected_cache_tags); + + \Drupal::configFactory() + ->getEditable('stark.settings') + ->set('third_party_settings.shortcut.module_link', TRUE) + ->save(TRUE); + + // Add cron to the default shortcut set, now the shortcut list cache tag + // is expected. + $this->drupalGet('admin/config/system/cron'); + $this->clickLink('Add to Default shortcuts'); + $expected_cache_tags[] = 'config:shortcut_set_list'; + $this->assertCacheTags($expected_cache_tags); + + // Verify that users without the 'access shortcuts' permission can't see the + // shortcuts. + $this->drupalLogin($this->drupalCreateUser(['access toolbar'])); + $this->assertNoLink('Shortcuts'); + $this->verifyDynamicPageCache($test_page_url, 'MISS'); + $this->verifyDynamicPageCache($test_page_url, 'HIT'); + + // Verify that users without the 'administer site configuration' permission + // can't see the cron shortcut but can see shortcuts toolbar tab. + $this->drupalLogin($this->drupalCreateUser([ + 'access toolbar', + 'access shortcuts', + ])); + $this->verifyDynamicPageCache($test_page_url, 'MISS'); + $this->verifyDynamicPageCache($test_page_url, 'HIT'); + $this->assertLink('Shortcuts'); + $this->assertNoLink('Cron'); + + // Create a role with access to shortcuts as well as the necessary + // permissions to see specific shortcuts. + $site_configuration_role = $this->drupalCreateRole([ + 'access toolbar', + 'access shortcuts', + 'administer site configuration', + 'access administration pages', + ]); + + // Create two different users with the same role to assert that the second + // user has a cache hit despite the user cache context, as + // the returned cache contexts include those from lazy-builder content. + $site_configuration_user1 = $this->drupalCreateUser(); + $site_configuration_user1->addRole($site_configuration_role); + $site_configuration_user1->save(); + $site_configuration_user2 = $this->drupalCreateUser(); + $site_configuration_user2->addRole($site_configuration_role); + $site_configuration_user2->save(); + + $this->drupalLogin($site_configuration_user1); + $this->verifyDynamicPageCache($test_page_url, 'MISS'); + $this->verifyDynamicPageCache($test_page_url, 'HIT'); + $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format']); + $this->assertLink('Shortcuts'); + $this->assertLink('Cron'); + + $this->drupalLogin($site_configuration_user2); + $this->verifyDynamicPageCache($test_page_url, 'HIT'); + $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format']); + $this->assertLink('Shortcuts'); + $this->assertLink('Cron'); + + // Add another shortcut. + $shortcut = Shortcut::create([ + 'shortcut_set' => 'default', + 'title' => 'Llama', + 'weight' => 0, + 'link' => [['uri' => 'internal:/admin/config']], + ]); + $shortcut->save(); + + // The shortcuts are displayed in a lazy builder, so the page is still a + // cache HIT but shows the new shortcut immediately. + $this->verifyDynamicPageCache($test_page_url, 'HIT'); + $this->assertLink('Cron'); + $this->assertLink('Llama'); + + // Update the shortcut title and assert that it is updated. + $shortcut->set('title', 'Alpaca'); + $shortcut->save(); + $this->verifyDynamicPageCache($test_page_url, 'HIT'); + $this->assertLink('Cron'); + $this->assertLink('Alpaca'); + + // Delete the shortcut and assert that the link is gone. + $shortcut->delete(); + $this->verifyDynamicPageCache($test_page_url, 'HIT'); + $this->assertLink('Cron'); + $this->assertNoLink('Alpaca'); + } + } diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php index 8f7bf0aa1bc7ae51354980e71db9e7d19343ced1..9342b329f2395d2b00cd73a482db9b4651f99ec8 100644 --- a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php @@ -355,9 +355,9 @@ public function testAccessShortcutsPermission() { $this->assertNoLink('Shortcuts', 'Shortcut link not found on page.'); // Verify that users without the 'administer site configuration' permission - // can't see the cron shortcuts. + // can't see the cron shortcuts but can see shortcuts. $this->drupalLogin($this->drupalCreateUser(['access toolbar', 'access shortcuts'])); - $this->assertNoLink('Shortcuts', 'Shortcut link not found on page.'); + $this->assertLink('Shortcuts'); $this->assertNoLink('Cron', 'Cron shortcut link not found on page.'); // Verify that users with the 'access shortcuts' permission can see the diff --git a/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php b/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php index 92ef5531b10f8e72e20f0b69c00e690ee705c35b..5398e218de81b7ada7f3e029a87dd4d6d91338c7 100644 --- a/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php +++ b/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php @@ -37,7 +37,6 @@ protected function setUp() { * The page for this URL will be loaded. * @param string $hit_or_miss * 'HIT' if a page cache hit is expected, 'MISS' otherwise. - * * @param array|false $tags * When expecting a page cache hit, you may optionally specify an array of * expected cache tags. While FALSE, the cache tags will not be verified. @@ -59,4 +58,18 @@ protected function verifyPageCache(Url $url, $hit_or_miss, $tags = FALSE) { } } + /** + * Verify that when loading a given page, it's a page cache hit or miss. + * + * @param \Drupal\Core\Url $url + * The page for this URL will be loaded. + * @param string $hit_or_miss + * 'HIT' if a page cache hit is expected, 'MISS' otherwise. + */ + protected function verifyDynamicPageCache(Url $url, $hit_or_miss) { + $this->drupalGet($url); + $message = new FormattableMarkup('Dynamic page cache @hit_or_miss for %path.', ['@hit_or_miss' => $hit_or_miss, '%path' => $url->toString()]); + $this->assertSame($hit_or_miss, $this->getSession()->getResponseHeader('X-Drupal-Dynamic-Cache'), $message); + } + } diff --git a/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php b/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php index 8b564cca07d6185b3fbeaac545710a5ca1476e0f..fbc8ee4a44e1d9f5c8fae883ad4163acb04f1140 100644 --- a/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php +++ b/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php @@ -98,11 +98,6 @@ public function testToolbarCacheContextsCaller() { $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->installExtraModules(['shortcut']); - $this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access shortcuts', 'administer shortcuts'])); - $this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found with shortcut module enabled.'); } /**