Issue #3427046 by plopesc, smustgrave, alexpott: Shortcuts toolbar links are not updated automatically when default shortcut set is changed

(cherry picked from commit 30a1adec4c)
merge-requests/7287/head
catch 2024-04-01 09:41:23 +01:00
parent 999d98d63a
commit 26f98e1612
14 changed files with 195 additions and 48 deletions

View File

@ -486,6 +486,8 @@ function drupal_static_reset($name = NULL) {
switch ($name) {
case 'system_get_module_admin_tasks':
@trigger_error("Calling " . __FUNCTION__ . "() with 'system_get_module_admin_tasks' as an argument is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. There is no replacement for this usage. See https://www.drupal.org/node/3038972", E_USER_DEPRECATED);
case 'shortcut_current_displayed_set':
@trigger_error("Calling " . __FUNCTION__ . "() with 'shortcut_current_displayed_set' as an argument is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no replacement for this usage. See https://www.drupal.org/node/3427050", E_USER_DEPRECATED);
break;
}
drupal_static($name, NULL, TRUE);

View File

@ -275,8 +275,8 @@ function jsonapi_jsonapi_node_filter_access(EntityTypeInterface $entity_type, Ac
function jsonapi_jsonapi_shortcut_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
// @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess()
// \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
// (shortcut_set = shortcut_current_displayed_set()), so this does not have
// to.
// (shortcut_set = $shortcut_set_storage->getDisplayedToUser($current_user)),
// so this does not have to.
return ([
JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')
->orIf(AccessResult::allowedIfHasPermissions($account, ['access shortcuts', 'customize shortcut links'])),

View File

@ -294,7 +294,8 @@ class TemporaryQueryGuard {
// user's currently displayed shortcut set.
// @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess()
if (!$current_user->hasPermission('administer shortcuts')) {
$specific_condition = new EntityCondition('shortcut_set', shortcut_current_displayed_set()->id());
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$specific_condition = new EntityCondition('shortcut_set', $shortcut_set_storage->getDisplayedToUser($current_user)->id());
$cacheability->addCacheContexts(['user']);
$cacheability->addCacheTags($entity_type->getListCacheTags());
}

View File

@ -12,7 +12,6 @@ use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\shortcut\Entity\ShortcutSet;
use Drupal\shortcut\ShortcutSetInterface;
/**
@ -66,7 +65,13 @@ function shortcut_set_edit_access(ShortcutSetInterface $shortcut_set = NULL) {
// Sufficiently-privileged users can edit their currently displayed shortcut
// set, but not other sets. They must also be able to access shortcuts.
$may_edit_current_shortcut_set = $account->hasPermission('customize shortcut links') && (!isset($shortcut_set) || $shortcut_set == shortcut_current_displayed_set()) && $account->hasPermission('access shortcuts');
$may_edit_current_shortcut_set = $account->hasPermission('customize shortcut links') && $account->hasPermission('access shortcuts');
if ($may_edit_current_shortcut_set && isset($shortcut_set)) {
$displayed_shortcut_set = \Drupal::entityTypeManager()
->getStorage('shortcut_set')
->getDisplayedToUser($account);
$may_edit_current_shortcut_set = ($shortcut_set == $displayed_shortcut_set);
}
$result = AccessResult::allowedIf($may_edit_current_shortcut_set)->cachePerPermissions();
if (!$result->isAllowed()) {
$result->setReason("The shortcut set must be the currently displayed set for the user and the user must have 'access shortcuts' AND 'customize shortcut links' permissions.");
@ -127,8 +132,15 @@ function shortcut_set_switch_access($account = NULL) {
* An object representing the shortcut set that should be displayed to the
* current user. If the user does not have an explicit shortcut set defined,
* the default set is returned.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\shortcut\ShortcutSetStorageInterface::getDisplayedToUser()
* instead.
*
* @see https://www.drupal.org/node/3427050
*/
function shortcut_current_displayed_set($account = NULL) {
@trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\shortcut\ShortcutSetStorageInterface::getDisplayedToUser() instead. See https://www.drupal.org/node/3427050', E_USER_DEPRECATED);
$shortcut_sets = &drupal_static(__FUNCTION__, []);
$user = \Drupal::currentUser();
if (!isset($account)) {
@ -140,16 +152,9 @@ function shortcut_current_displayed_set($account = NULL) {
}
// If none was found, try to find a shortcut set that is explicitly assigned
// to this user.
$shortcut_set_name = \Drupal::entityTypeManager()
$shortcut_set = \Drupal::entityTypeManager()
->getStorage('shortcut_set')
->getAssignedToUser($account);
if ($shortcut_set_name) {
$shortcut_set = ShortcutSet::load($shortcut_set_name);
}
// Otherwise, use the default set.
else {
$shortcut_set = shortcut_default_set($account);
}
->getDisplayedToUser($account);
$shortcut_sets[$account->id()] = $shortcut_set;
return $shortcut_set;
@ -165,26 +170,22 @@ function shortcut_current_displayed_set($account = NULL) {
*
* @return \Drupal\shortcut\ShortcutSetInterface|null
* An object representing the default shortcut set.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\shortcut\ShortcutSetStorageInterface::getDefaultSet() instead.
*
* @see https://www.drupal.org/node/3427050
*/
function shortcut_default_set($account = NULL) {
@trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\shortcut\ShortcutSetStorageInterface::getDefaultSet() instead. See https://www.drupal.org/node/3427050', E_USER_DEPRECATED);
$user = \Drupal::currentUser();
if (!isset($account)) {
$account = $user;
}
// Allow modules to return a default shortcut set name. Since we can only
// have one, we allow the last module which returns a valid result to take
// precedence. If no module returns a valid set, fall back on the site-wide
// default, which is the lowest-numbered shortcut set.
$suggestions = array_reverse(\Drupal::moduleHandler()->invokeAll('shortcut_default_set', [$account]));
$suggestions[] = 'default';
foreach ($suggestions as $name) {
if ($shortcut_set = ShortcutSet::load($name)) {
break;
}
}
return $shortcut_set;
return \Drupal::entityTypeManager()
->getStorage('shortcut_set')
->getDefaultSet($account);
}
/**
@ -201,7 +202,10 @@ function shortcut_renderable_links($shortcut_set = NULL) {
$shortcut_links = [];
if (!isset($shortcut_set)) {
$shortcut_set = shortcut_current_displayed_set();
$account = \Drupal::currentUser();
$shortcut_set = \Drupal::entityTypeManager()
->getStorage('shortcut_set')
->getDisplayedToUser($account);
}
$cache_tags = [];
@ -266,7 +270,9 @@ function shortcut_preprocess_page_title(&$variables) {
'name' => trim(strip_tags($name)),
];
$shortcut_set = shortcut_current_displayed_set();
$shortcut_set = \Drupal::entityTypeManager()
->getStorage('shortcut_set')
->getDisplayedToUser(\Drupal::currentUser());
// Pages with the add or remove shortcut button need cache invalidation when
// a shortcut is added, edited, or removed.
@ -334,7 +340,9 @@ function shortcut_toolbar() {
];
if ($user->hasPermission('access shortcuts')) {
$shortcut_set = shortcut_current_displayed_set();
$shortcut_set = \Drupal::entityTypeManager()
->getStorage('shortcut_set')
->getDisplayedToUser($user);
$items['shortcuts'] += [
'#type' => 'toolbar_item',

View File

@ -1,5 +1,5 @@
services:
shortcut.lazy_builders:
class: Drupal\shortcut\ShortcutLazyBuilders
arguments: ['@renderer']
arguments: ['@renderer', '@entity_type.manager', '@current_user']
Drupal\shortcut\ShortcutLazyBuilders: '@shortcut.lazy_builders'

View File

@ -73,7 +73,7 @@ class ShortcutSet extends ConfigEntityBundleBase implements ShortcutSetInterface
if (!$update && !$this->isSyncing()) {
// Save a new shortcut set with links copied from the user's default set.
$default_set = shortcut_default_set();
$default_set = $storage->getDefaultSet(\Drupal::currentUser());
// This is the default set, do not copy shortcuts.
if ($default_set->id() != $this->id()) {
foreach ($default_set->getShortcuts() as $shortcut) {

View File

@ -70,7 +70,7 @@ class SwitchShortcutSet extends FormBase {
return $set->label();
}, $this->shortcutSetStorage->loadMultiple());
$current_set = shortcut_current_displayed_set($this->user);
$current_set = $this->shortcutSetStorage->getDisplayedToUser($this->user);
// Only administrators can add shortcut sets.
$add_access = $account->hasPermission('administer shortcuts');

View File

@ -2,8 +2,10 @@
namespace Drupal\shortcut;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
/**
@ -23,9 +25,21 @@ class ShortcutLazyBuilders implements TrustedCallbackInterface {
*
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface|null $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Session\AccountInterface|null $currentUser
* The current user.
*/
public function __construct(RendererInterface $renderer) {
public function __construct(RendererInterface $renderer, protected ?EntityTypeManagerInterface $entityTypeManager, protected ?AccountInterface $currentUser) {
$this->renderer = $renderer;
if (!isset($this->entityTypeManager)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $entityTypeManager argument is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3427050', E_USER_DEPRECATED);
$this->entityTypeManager = \Drupal::entityTypeManager();
}
if (!isset($this->currentUser)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $currentUser argument is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3427050', E_USER_DEPRECATED);
$this->currentUser = \Drupal::currentUser();
}
}
/**
@ -45,7 +59,8 @@ class ShortcutLazyBuilders implements TrustedCallbackInterface {
* A renderable array of shortcut links.
*/
public function lazyLinks(bool $show_configure_link = TRUE) {
$shortcut_set = shortcut_current_displayed_set();
$shortcut_set = $this->entityTypeManager->getStorage('shortcut_set')
->getDisplayedToUser($this->currentUser);
$links = shortcut_renderable_links();
@ -63,6 +78,7 @@ class ShortcutLazyBuilders implements TrustedCallbackInterface {
'shortcuts' => $links,
'configure' => $configure_link,
];
$this->renderer->addCacheableDependency($build, $shortcut_set);
return $build;

View File

@ -3,16 +3,42 @@
namespace Drupal\shortcut;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access control handler for the shortcut set entity type.
*
* @see \Drupal\shortcut\Entity\ShortcutSet
*/
class ShortcutSetAccessControlHandler extends EntityAccessControlHandler {
class ShortcutSetAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
/**
* Constructs a ShortcutSetAccessControlHandler object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(EntityTypeInterface $entity_type, protected EntityTypeManagerInterface $entityTypeManager) {
parent::__construct($entity_type);
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
@ -29,7 +55,9 @@ class ShortcutSetAccessControlHandler extends EntityAccessControlHandler {
if (!$account->hasPermission('access shortcuts')) {
return AccessResult::neutral()->cachePerPermissions();
}
return AccessResult::allowedIf($account->hasPermission('customize shortcut links') && $entity == shortcut_current_displayed_set($account))->cachePerPermissions()->addCacheableDependency($entity);
$shortcut_set_storage = $this->entityTypeManager->getStorage('shortcut_set');
return AccessResult::allowedIf($account->hasPermission('customize shortcut links') && $entity == $shortcut_set_storage->getDisplayedToUser($account))->cachePerPermissions()->addCacheableDependency($entity);
case 'delete':
return AccessResult::allowedIf($account->hasPermission('administer shortcuts') && $entity->id() != 'default')->cachePerPermissions();

View File

@ -3,6 +3,7 @@
namespace Drupal\shortcut;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
@ -88,20 +89,27 @@ class ShortcutSetStorage extends ConfigEntityStorage implements ShortcutSetStora
* {@inheritdoc}
*/
public function assignUser(ShortcutSetInterface $shortcut_set, $account) {
$current_shortcut_set = $this->getDisplayedToUser($account);
$this->connection->merge('shortcut_set_users')
->key('uid', $account->id())
->fields(['set_name' => $shortcut_set->id()])
->execute();
drupal_static_reset('shortcut_current_displayed_set');
if ($current_shortcut_set instanceof ShortcutSetInterface) {
Cache::invalidateTags($current_shortcut_set->getCacheTagsToInvalidate());
}
}
/**
* {@inheritdoc}
*/
public function unassignUser($account) {
$current_shortcut_set = $this->getDisplayedToUser($account);
$deleted = $this->connection->delete('shortcut_set_users')
->condition('uid', $account->id())
->execute();
if ($current_shortcut_set instanceof ShortcutSetInterface) {
Cache::invalidateTags($current_shortcut_set->getCacheTagsToInvalidate());
}
return (bool) $deleted;
}
@ -115,6 +123,17 @@ class ShortcutSetStorage extends ConfigEntityStorage implements ShortcutSetStora
return $query->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function getDisplayedToUser(AccountInterface $account): ShortcutSetInterface {
if ($set_name = $this->getAssignedToUser($account)) {
return $this->load($set_name);
}
return $this->getDefaultSet($account);
}
/**
* {@inheritdoc}
*/

View File

@ -54,6 +54,17 @@ interface ShortcutSetStorageInterface extends ConfigEntityStorageInterface {
*/
public function getAssignedToUser($account);
/**
* Gets the shortcut set to be displayed for a given user account.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The user account whose default shortcut set will be returned.
*
* @return \Drupal\shortcut\ShortcutSetInterface
* An object representing the default shortcut set.
*/
public function getDisplayedToUser(AccountInterface $account): ShortcutSetInterface;
/**
* Get the number of users who have this set assigned to them.
*

View File

@ -7,6 +7,7 @@ namespace Drupal\Tests\shortcut\Functional;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Url;
use Drupal\shortcut\Entity\Shortcut;
use Drupal\shortcut\Entity\ShortcutSet;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase;
use Drupal\user\Entity\Role;
@ -203,6 +204,62 @@ class ShortcutCacheTagsTest extends EntityCacheTagsTestBase {
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('Alpaca');
// Add a new Shortcut Set with a single link.
$new_set = ShortcutSet::create([
'id' => 'llama-set',
'label' => 'Llama Set',
]);
$new_set->save();
$new_shortcut = Shortcut::create([
'shortcut_set' => 'llama-set',
'title' => 'New Llama',
'weight' => 0,
'link' => [['uri' => 'internal:/admin/config']],
]);
$new_shortcut->save();
// Assign the new shortcut set to user 2 and confirm that links are
// changed automatically.
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->assignUser($new_set, $site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('New Llama');
// Confirm that links for user 1 have not been affected.
$this->drupalLogin($site_configuration_user1);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('New Llama');
// Confirm that removing assignment automatically changes the links too.
$this->drupalLogin($site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('New Llama');
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->unassignUser($site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('New Llama');
// Confirm that deleting a shortcut set automatically changes the links too.
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->assignUser($new_set, $site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('New Llama');
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->delete([$new_set]);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('New Llama');
}
/**

View File

@ -114,7 +114,8 @@ class ShortcutSetsTest extends ShortcutTestBase {
$this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts');
$this->submitForm(['set' => $new_set->id()], 'Change set');
$this->assertSession()->statusCodeEquals(200);
$current_set = shortcut_current_displayed_set($this->adminUser);
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$current_set = $shortcut_set_storage->getDisplayedToUser($this->adminUser);
$this->assertSame($current_set->id(), $new_set->id(), 'Successfully switched own shortcut set.');
}
@ -124,8 +125,9 @@ class ShortcutSetsTest extends ShortcutTestBase {
public function testShortcutSetAssign() {
$new_set = $this->generateShortcutSet($this->randomMachineName());
\Drupal::entityTypeManager()->getStorage('shortcut_set')->assignUser($new_set, $this->shortcutUser);
$current_set = shortcut_current_displayed_set($this->shortcutUser);
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$shortcut_set_storage->assignUser($new_set, $this->shortcutUser);
$current_set = $shortcut_set_storage->getDisplayedToUser($this->shortcutUser);
$this->assertSame($current_set->id(), $new_set->id(), "Successfully switched another user's shortcut set.");
}
@ -140,7 +142,8 @@ class ShortcutSetsTest extends ShortcutTestBase {
];
$this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts');
$this->submitForm($edit, 'Change set');
$current_set = shortcut_current_displayed_set($this->adminUser);
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$current_set = $shortcut_set_storage->getDisplayedToUser($this->adminUser);
$this->assertNotEquals($this->set->id(), $current_set->id(), 'A shortcut set can be switched to at the same time as it is created.');
$this->assertEquals($edit['label'], $current_set->label(), 'The new set is correctly assigned to the user.');
}
@ -153,7 +156,8 @@ class ShortcutSetsTest extends ShortcutTestBase {
$this->drupalGet('user/' . $this->adminUser->id() . '/shortcuts');
$this->submitForm($edit, 'Change set');
$this->assertSession()->pageTextContains('The new set label is required.');
$current_set = shortcut_current_displayed_set($this->adminUser);
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$current_set = $shortcut_set_storage->getDisplayedToUser($this->adminUser);
$this->assertEquals($this->set->id(), $current_set->id(), 'Attempting to switch to a new shortcut set without providing a set name does not succeed.');
$field = $this->assertSession()->fieldExists('label');
$this->assertTrue($field->hasClass('error'));
@ -182,8 +186,8 @@ class ShortcutSetsTest extends ShortcutTestBase {
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$shortcut_set_storage->assignUser($new_set, $this->shortcutUser);
$shortcut_set_storage->unassignUser($this->shortcutUser);
$current_set = shortcut_current_displayed_set($this->shortcutUser);
$default_set = shortcut_default_set($this->shortcutUser);
$current_set = $shortcut_set_storage->getDisplayedToUser($this->shortcutUser);
$default_set = $shortcut_set_storage->getDefaultSet($this->shortcutUser);
$this->assertSame($default_set->id(), $current_set->id(), "Successfully unassigned another user's shortcut set.");
}
@ -196,8 +200,8 @@ class ShortcutSetsTest extends ShortcutTestBase {
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$shortcut_set_storage->assignUser($new_set, $this->shortcutUser);
$this->shortcutUser->delete();
$current_set = shortcut_current_displayed_set($this->shortcutUser);
$default_set = shortcut_default_set($this->shortcutUser);
$current_set = $shortcut_set_storage->getDisplayedToUser($this->shortcutUser);
$default_set = $shortcut_set_storage->getDefaultSet($this->shortcutUser);
$this->assertSame($default_set->id(), $current_set->id(), "Successfully cleared assigned shortcut set for removed user.");
}

View File

@ -45,7 +45,8 @@ class MigrateShortcutSetUsersTest extends MigrateDrupal7TestBase {
public function testShortcutSetUsersMigration() {
// Check if migrated user has correct migrated shortcut set assigned.
$account = User::load(2);
$shortcut_set = shortcut_current_displayed_set($account);
$shortcut_set_storage = \Drupal::entityTypeManager()->getStorage('shortcut_set');
$shortcut_set = $shortcut_set_storage->getDisplayedToUser($account);
/** @var \Drupal\shortcut\ShortcutSetInterface $shortcut_set */
$this->assertSame('shortcut-set-2', $shortcut_set->id());
}