diff --git a/core/modules/content_moderation/tests/src/Kernel/WorkspacesContentModerationStateTest.php b/core/modules/content_moderation/tests/src/Kernel/WorkspacesContentModerationStateTest.php index ebdfd4cdbc..09e6cdbcd7 100644 --- a/core/modules/content_moderation/tests/src/Kernel/WorkspacesContentModerationStateTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/WorkspacesContentModerationStateTest.php @@ -164,17 +164,6 @@ public function basicModerationTestCases() { ]; } - /** - * {@inheritdoc} - */ - public function testContentModerationStateDataRemoval($entity_type_id = NULL) { - // This test creates published default revisions in Live, which can not be - // deleted in a workspace. A test scenario for the case when Content - // Moderation and Workspaces are used together is covered in - // parent::testContentModerationStateRevisionDataRemoval(). - $this->markTestSkipped(); - } - /** * {@inheritdoc} */ diff --git a/core/modules/workspaces/src/EntityAccess.php b/core/modules/workspaces/src/EntityAccess.php index 4d7461a9b0..db7b6ed5ff 100644 --- a/core/modules/workspaces/src/EntityAccess.php +++ b/core/modules/workspaces/src/EntityAccess.php @@ -33,13 +33,6 @@ class EntityAccess implements ContainerInjectionInterface { */ protected $workspaceManager; - /** - * The workspace association service. - * - * @var \Drupal\workspaces\WorkspaceAssociationInterface - */ - protected $workspaceAssociation; - /** * Constructs a new EntityAccess instance. * @@ -47,13 +40,10 @@ class EntityAccess implements ContainerInjectionInterface { * The entity type manager service. * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager * The workspace manager service. - * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association - * The workspace association service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) { $this->entityTypeManager = $entity_type_manager; $this->workspaceManager = $workspace_manager; - $this->workspaceAssociation = $workspace_association; } /** @@ -62,8 +52,7 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Wor public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), - $container->get('workspaces.manager'), - $container->get('workspaces.association') + $container->get('workspaces.manager') ); } @@ -90,16 +79,6 @@ public function entityOperationAccess(EntityInterface $entity, $operation, Accou return AccessResult::neutral(); } - // Prevent the deletion of entities with a published default revision. - if ($operation === 'delete') { - $active_workspace = $this->workspaceManager->getActiveWorkspace(); - $is_deletable = $this->workspaceAssociation->isEntityDeletable($entity, $active_workspace); - - return AccessResult::forbiddenIf(!$is_deletable) - ->addCacheableDependency($entity) - ->addCacheableDependency($active_workspace); - } - return $this->bypassAccessResult($account); } diff --git a/core/modules/workspaces/src/EntityOperations.php b/core/modules/workspaces/src/EntityOperations.php index 28ac7210d1..b8894643c7 100644 --- a/core/modules/workspaces/src/EntityOperations.php +++ b/core/modules/workspaces/src/EntityOperations.php @@ -136,11 +136,11 @@ public function entityPresave(EntityInterface $entity) { // become the default revision only when it is replicated to the default // workspace. $entity->isDefaultRevision(FALSE); - } - // Track the workspaces in which the new revision was saved. - $field_name = $entity_type->getRevisionMetadataKey('workspace'); - $entity->{$field_name}->target_id = $this->workspaceManager->getActiveWorkspace()->id(); + // Track the workspaces in which the new revision was saved. + $field_name = $entity_type->getRevisionMetadataKey('workspace'); + $entity->{$field_name}->target_id = $this->workspaceManager->getActiveWorkspace()->id(); + } // When a new published entity is inserted in a non-default workspace, we // actually want two revisions to be saved: @@ -231,12 +231,10 @@ public function entityPredelete(EntityInterface $entity) { return; } - // Prevent the entity from being deleted if the entity type does not have - // support for workspaces, or if the entity has a published default - // revision. - $active_workspace = $this->workspaceManager->getActiveWorkspace(); - if (!$this->workspaceManager->isEntityTypeSupported($entity_type) || !$this->workspaceAssociation->isEntityDeletable($entity, $active_workspace)) { - throw new \RuntimeException('This entity can only be deleted in the Live workspace.'); + // Disallow any change to an unsupported entity when we are not in the + // default workspace. + if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) { + throw new \RuntimeException('This entity can only be deleted in the default workspace.'); } } @@ -273,17 +271,6 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, $f $form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild']; } - // Prevent entities from being deleted in a workspace if they have a - // published default revision. - $active_workspace = $this->workspaceManager->getActiveWorkspace(); - if ($form_state->getFormObject()->getOperation() === 'delete' && $active_workspace && !$this->workspaceAssociation->isEntityDeletable($entity, $active_workspace)) { - $form['#markup'] = $this->t('This @entity_type_label can only be deleted in the Live workspace.', [ - '@entity_type_label' => $entity->getEntityType()->getSingularLabel(), - ]); - $form['#access'] = FALSE; - return; - } - // Run the workspace conflict validation constraint when the entity form is // being built so we can "disable" it early and display a message to the // user, instead of allowing them to enter data that can never be saved. diff --git a/core/modules/workspaces/src/EntityQuery/QueryTrait.php b/core/modules/workspaces/src/EntityQuery/QueryTrait.php index d46e1aba12..c28ede5a36 100644 --- a/core/modules/workspaces/src/EntityQuery/QueryTrait.php +++ b/core/modules/workspaces/src/EntityQuery/QueryTrait.php @@ -52,9 +52,9 @@ public function prepare() { return $this; } - // Only alter the query if we have an active workspace, the entity type is - // supported and we weren't instructed to skip altering it. - if ($this->workspaceManager->hasActiveWorkspace() && $this->workspaceManager->isEntityTypeSupported($this->entityType) && !$this->sqlQuery->getMetaData('skip_workspaces_alter')) { + // Only alter the query if the active workspace is not the default one and + // the entity type is supported. + if ($this->workspaceManager->hasActiveWorkspace() && $this->workspaceManager->isEntityTypeSupported($this->entityType)) { $active_workspace = $this->workspaceManager->getActiveWorkspace(); $this->sqlQuery->addMetaData('active_workspace_id', $active_workspace->id()); $this->sqlQuery->addMetaData('simple_query', FALSE); diff --git a/core/modules/workspaces/src/WorkspaceAssociation.php b/core/modules/workspaces/src/WorkspaceAssociation.php index 41e62b53bb..9b9b2f01dd 100644 --- a/core/modules/workspaces/src/WorkspaceAssociation.php +++ b/core/modules/workspaces/src/WorkspaceAssociation.php @@ -38,16 +38,6 @@ class WorkspaceAssociation implements WorkspaceAssociationInterface { */ protected $workspaceRepository; - /** - * A multidimensional array of entity IDs that can be deleted in a workspace. - * - * The first level keys are workspace IDs, the second level keys are entity - * type IDs, and the third level array contains entity IDs. - * - * @var array - */ - protected $deletableEntityIds = []; - /** * Constructs a WorkspaceAssociation object. * @@ -124,8 +114,6 @@ public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $w watchdog_exception('workspaces', $e); throw $e; } - - $this->deletableEntityIds = []; } /** @@ -191,7 +179,7 @@ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_i $query ->fields('revision', [$revision_id_field, $id_field]) ->condition("revision.$workspace_field", $workspace_id) - ->where("revision.$revision_id_field >= base.$revision_id_field") + ->where("revision.$revision_id_field > base.$revision_id_field") ->orderBy("revision.$revision_id_field", 'ASC'); // Restrict the result to a set of entity ID's if provided. @@ -237,7 +225,6 @@ public function deleteAssociations($workspace_id, $entity_type_id = NULL, $entit } $query->execute(); - $this->deletableEntityIds = []; } /** @@ -259,59 +246,4 @@ public function initializeWorkspace(WorkspaceInterface $workspace) { } } - /** - * {@inheritdoc} - */ - public function isEntityDeletable(RevisionableInterface $entity, WorkspaceInterface $workspace) { - // Initialize all the deletable entity IDs upfront for performance reasons. - // This is needed for admin listing pages, which usually do this check for - // at least 50 entities. - if (!isset($this->deletableEntityIds[$workspace->id()][$entity->getEntityTypeId()])) { - // First, get all the unpublished default revisions. - $unpublished_default_revisions = $this->entityTypeManager->getStorage($entity->getEntityTypeId()) - ->getQuery() - ->accessCheck(FALSE) - ->condition($entity->getEntityType()->getKey('published'), 0) - ->addMetaData('skip_workspaces_alter', TRUE) - ->execute(); - - // If there are no unpublished default revisions, the entity can not be - // deleted. - if (!$unpublished_default_revisions) { - $this->deletableEntityIds[$workspace->id()][$entity->getEntityTypeId()] = []; - return FALSE; - } - - // Next, get the revisions count for all the entity IDs found above. - $id_key = $entity->getEntityType()->getKey('id'); - $revision_id_key = $entity->getEntityType()->getKey('revision'); - $revision_count_key = 'revision_count'; - $revisions_count = $this->entityTypeManager->getStorage($entity->getEntityTypeId()) - ->getAggregateQuery() - ->allRevisions() - ->accessCheck(FALSE) - ->condition($id_key, $unpublished_default_revisions, 'IN') - ->aggregate($revision_id_key, 'COUNT', NULL, $revision_count_key) - ->groupBy($id_key) - ->execute(); - - // Now get all the revisions associated with the given workspace. - $associated_revisions = $this->getAssociatedRevisions($workspace->id(), $entity->getEntityTypeId(), $unpublished_default_revisions); - $associated_revisions = array_count_values($associated_revisions) + array_fill_keys($unpublished_default_revisions, 0); - - // Finally, compare the number of revisions associated with the active - // workspace with the total number of revisions for each entity. If the - // total number of revisions equals the workspace associated revisions, - // the entity was only edited in a single workspace and therefore can be - // deleted. - foreach ($revisions_count as $revision_count_value) { - if ((int) $revision_count_value[$revision_count_key] === $associated_revisions[$revision_count_value[$id_key]]) { - $this->deletableEntityIds[$workspace->id()][$entity->getEntityTypeId()][$revision_count_value[$id_key]] = $revision_count_value[$id_key]; - } - } - } - - return isset($this->deletableEntityIds[$workspace->id()][$entity->getEntityTypeId()][$entity->id()]); - } - } diff --git a/core/modules/workspaces/src/WorkspaceAssociationInterface.php b/core/modules/workspaces/src/WorkspaceAssociationInterface.php index f11bc5cdbd..5ad45babf2 100644 --- a/core/modules/workspaces/src/WorkspaceAssociationInterface.php +++ b/core/modules/workspaces/src/WorkspaceAssociationInterface.php @@ -116,17 +116,4 @@ public function deleteAssociations($workspace_id, $entity_type_id = NULL, $entit */ public function initializeWorkspace(WorkspaceInterface $workspace); - /** - * Determines whether an entity can be deleted in the given workspace. - * - * @param \Drupal\Core\Entity\RevisionableInterface $entity - * The entity object which needs to be checked. - * @param \Drupal\workspaces\WorkspaceInterface $workspace - * The workspace in which the entity needs to be checked. - * - * @return bool - * TRUE if the entity can be deleted, FALSE otherwise. - */ - public function isEntityDeletable(RevisionableInterface $entity, WorkspaceInterface $workspace); - } diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceEntityDeleteTest.php b/core/modules/workspaces/tests/src/Functional/WorkspaceEntityDeleteTest.php deleted file mode 100644 index 3e0d2f38d6..0000000000 --- a/core/modules/workspaces/tests/src/Functional/WorkspaceEntityDeleteTest.php +++ /dev/null @@ -1,162 +0,0 @@ -createContentType(['type' => 'article', 'label' => 'Article']); - $this->setupWorkspaceSwitcherBlock(); - } - - /** - * Test entity deletion with workspaces. - */ - public function testEntityDelete() { - $assert_session = $this->assertSession(); - - $permissions = [ - 'administer workspaces', - 'create workspace', - 'access content overview', - 'administer nodes', - 'create article content', - 'edit own article content', - 'delete own article content', - 'view own unpublished content', - ]; - $editor = $this->drupalCreateUser($permissions); - $this->drupalLogin($editor); - - // Create a Dev workspace as a child of Stage. - $stage = Workspace::load('stage'); - $dev = $this->createWorkspaceThroughUi('Dev', 'dev', 'stage'); - - // Create a published and an unpublished node in Live. - $published_live = $this->createNodeThroughUi('Test 1 published - live', 'article'); - $unpublished_live = $this->createNodeThroughUi('Test 2 unpublished - live', 'article', FALSE); - - // Create a published and an unpublished node in Stage. - $this->switchToWorkspace($stage); - $published_stage = $this->createNodeThroughUi('Test 3 published - stage', 'article'); - $unpublished_stage = $this->createNodeThroughUi('Test 4 unpublished - stage', 'article', FALSE); - - // Check that the Live nodes (both published and unpublished) can not be - // deleted, while the Stage nodes can be. - $this->drupalGet('admin/content'); - $assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefExists($published_stage->toUrl('delete-form')->toString()); - $assert_session->linkByHrefExists($unpublished_stage->toUrl('delete-form')->toString()); - - // Switch to Dev and check which nodes can be deleted. - $this->switchToWorkspace($dev); - $this->drupalGet('admin/content'); - - // The two Live nodes have the same deletable status as they had in Stage. - $assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString()); - - // The two Stage nodes should not be deletable in a child workspace (Dev). - $assert_session->linkByHrefNotExists($published_stage->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString()); - - // Edit $unpublished_stage in Dev and check that it is no longer deletable - // in Stage. - $this->drupalPostForm($unpublished_stage->toUrl('edit-form')->toString(), [], 'Save'); - $this->switchToWorkspace($stage); - $this->drupalGet('admin/content'); - $assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString()); - - // Add a new revision for each node and check that their 'deletable' status - // remains unchanged. - $this->drupalPostForm($published_live->toUrl('edit-form')->toString(), [], 'Save'); - $this->drupalPostForm($unpublished_live->toUrl('edit-form')->toString(), [], 'Save'); - $this->drupalPostForm($published_stage->toUrl('edit-form')->toString(), [], 'Save'); - - $this->drupalGet('admin/content'); - $assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefExists($published_stage->toUrl('delete-form')->toString()); - // $unpublished_stage has a workspace-specific revision in Dev, so it can't - // be edited nor deleted in Stage. - $assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString()); - - // Publish the Stage workspace and check that no entity can be deleted - // anymore in Stage nor Dev. - $stage->publish(); - $this->drupalGet('admin/content'); - $assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($published_stage->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString()); - - $this->switchToWorkspace($dev); - $this->drupalGet('admin/content'); - $assert_session->linkByHrefNotExists($published_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_live->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($published_stage->toUrl('delete-form')->toString()); - $assert_session->linkByHrefNotExists($unpublished_stage->toUrl('delete-form')->toString()); - } - - /** - * Test node deletion with workspaces and the 'bypass node access' permission. - */ - public function testNodeDeleteWithBypassAccessPermission() { - $assert_session = $this->assertSession(); - - $permissions = [ - 'administer workspaces', - 'create workspace', - 'access content overview', - 'bypass node access', - ]; - $editor = $this->drupalCreateUser($permissions); - $this->drupalLogin($editor); - - // Create a published node in Live. - $published_live = $this->createNodeThroughUi('Test 1 published - live', 'article'); - - $stage = Workspace::load('stage'); - $this->switchToWorkspace($stage); - - // A user with the 'bypass node access' permission will be able to see the - // 'Delete' operation button, but it shouldn't be able to perform the - // deletion. - $this->drupalGet('admin/content'); - $assert_session->linkByHrefExists($published_live->toUrl('delete-form')->toString()); - $this->clickLink('Delete'); - - $this->drupalGet($published_live->toUrl('delete-form')->toString()); - $assert_session->pageTextContains('This content item can only be deleted in the Live workspace.'); - $assert_session->buttonNotExists('Delete'); - - // Go back to Live and check the delete form is not affected by the - // workspace's delete protection. - $this->switchToLive(); - $this->drupalGet($published_live->toUrl('delete-form')->toString()); - $assert_session->pageTextNotContains('This content item can only be deleted in the Live workspace.'); - $assert_session->buttonExists('Delete'); - } - -} diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php index 081363eb26..b2f0d42d2a 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php @@ -667,7 +667,7 @@ public function testDisallowedEntityDeleteInNonDefaultWorkspace($entity_type_id, if (!$allowed) { $this->expectException(EntityStorageException::class); - $this->expectExceptionMessage('This entity can only be deleted in the Live workspace.'); + $this->expectExceptionMessage('This entity can only be deleted in the default workspace.'); } $entity->delete(); }