Loading core/modules/workspaces/src/Hook/WorkspacesHooks.php +34 −5 Original line number Diff line number Diff line Loading @@ -119,23 +119,52 @@ public function cron(): void { $associated_entity_storage = $this->entityTypeManager->getStorage($entity_type_id); // Sort the associated revisions in reverse ID order, so we can delete // the most recent revisions first. // the most recent revisions first, and only keep the revisions that // fit in the current batch. krsort($associated_revisions); $associated_revisions = array_slice($associated_revisions, 0, max(0, $batch_size - $count + 1), TRUE); // Get a list of default revisions tracked by the given workspace, // because they need to be handled differently than pending revisions. $initial_revision_ids = $this->workspaceTracker->getTrackedInitialRevisions($workspace_id, $entity_type_id); foreach (array_keys($associated_revisions) as $revision_id) { if ($count > $batch_size) { continue 2; // Load all the entities of this batch that are tracked through their // initial revision in a single storage call. $initial_revision_entities = []; if ($initial_entity_ids = array_intersect_key($initial_revision_ids, $associated_revisions)) { $initial_revision_entities = $associated_entity_storage->loadMultiple($initial_entity_ids); } foreach (array_keys($associated_revisions) as $revision_id) { // If the workspace is tracking the entity's default revision (i.e. // the entity was created inside that workspace), we need to delete // the whole entity after all of its pending revisions are gone. if (isset($initial_revision_ids[$revision_id])) { $associated_entity_storage->delete([$associated_entity_storage->load($initial_revision_ids[$revision_id])]); /** @var \Drupal\Core\Entity\RevisionableInterface&\Drupal\Core\Entity\EntityPublishedInterface $entity */ $entity = $initial_revision_entities[$initial_revision_ids[$revision_id]] ?? NULL; if (!$entity) { continue; } // The entity might have gone live after it was created in this // workspace. This can happen when the entity is edited in multiple // workspaces at the same time and one of the others is published. // An entity with a published default revision is live content, so // we only delete the revision created in this workspace, not the // whole entity. if ($entity->isPublished()) { if ($entity->getRevisionId() != $revision_id) { $associated_entity_storage->deleteRevision($revision_id); } else { // The default revision of an entity can not be deleted, so we // only remove its tracking records. $this->workspaceTracker->deleteTrackedEntities($workspace_id, $entity_type_id, [$entity->id()], [$revision_id]); } } else { $associated_entity_storage->delete([$entity]); } } else { // Delete the associated entity revision. Loading core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php +59 −0 Original line number Diff line number Diff line Loading @@ -280,6 +280,65 @@ public function testDeletingPublishedWorkspace(): void { $this->assertTrue($revisions[3]->isDefaultRevision()); $this->assertFalse(isset($revisions[4])); $this->assertFalse(isset($revisions[5])); // Check that deleting a workspace does not delete the entities which were // created in it but became live content in the meantime. $node_storage = $this->entityTypeManager->getStorage('node'); /** @var \Drupal\workspaces\WorkspaceTrackerInterface $workspace_tracker */ $workspace_tracker = \Drupal::service('workspaces.tracker'); $workspace_3 = Workspace::create(['id' => 'summer', 'label' => 'Summer']); $workspace_3->save(); $workspace_4 = Workspace::create(['id' => 'winter', 'label' => 'Winter']); $workspace_4->save(); // Create two new nodes in the 'summer' workspace. The published one gets an // unpublished initial revision (6) and a published pending revision (7), // while the unpublished one only gets an initial revision (8). $this->workspaceManager->setActiveWorkspace($workspace_3); $node_1 = $this->createNode(['status' => TRUE]); $node_2 = $this->createNode(['status' => FALSE]); // Simulate a module that bypasses the entity editing protections and // creates a new revision (9) of the first node in another workspace. $this->workspaceManager->setActiveWorkspace($workspace_4); $node_1 = $node_storage->load($node_1->id()); $node_1->setNewRevision(TRUE); $node_1->setPublished(); $node_1->save(); // Publishing the 'winter' workspace makes revision 9 the published default // revision of the first node. $workspace_4->publish(); // Publish the second node in Live without creating a new revision, so its // initial revision (8) is also its default revision. $this->workspaceManager->switchToLive(); $node_storage->resetCache(); $node_2 = $node_storage->load($node_2->id()); $node_2->setPublished(); $node_2->save(); // Deleting the 'summer' workspace has to purge only the revisions created // in it, because both nodes are live content now. $workspace_3->delete(); $node_storage->resetCache(); $node_1 = $node_storage->load($node_1->id()); $this->assertNotNull($node_1); $this->assertTrue($node_1->isPublished()); $this->assertEquals(9, $node_1->getRevisionId()); $this->assertEmpty($node_storage->loadMultipleRevisions([6, 7])); // The initial revision of the second node is also its default revision, so // it can not be deleted. Only its tracking records are removed. $node_2 = $node_storage->load($node_2->id()); $this->assertNotNull($node_2); $this->assertTrue($node_2->isPublished()); $this->assertEquals(8, $node_2->getRevisionId()); $this->assertEmpty($workspace_tracker->getAllTrackedRevisions($workspace_3->id(), 'node')); // The purge finished, so the workspace ID is no longer in the queue. $this->assertEmpty(\Drupal::state()->get('workspace.deleted')); } /** Loading Loading
core/modules/workspaces/src/Hook/WorkspacesHooks.php +34 −5 Original line number Diff line number Diff line Loading @@ -119,23 +119,52 @@ public function cron(): void { $associated_entity_storage = $this->entityTypeManager->getStorage($entity_type_id); // Sort the associated revisions in reverse ID order, so we can delete // the most recent revisions first. // the most recent revisions first, and only keep the revisions that // fit in the current batch. krsort($associated_revisions); $associated_revisions = array_slice($associated_revisions, 0, max(0, $batch_size - $count + 1), TRUE); // Get a list of default revisions tracked by the given workspace, // because they need to be handled differently than pending revisions. $initial_revision_ids = $this->workspaceTracker->getTrackedInitialRevisions($workspace_id, $entity_type_id); foreach (array_keys($associated_revisions) as $revision_id) { if ($count > $batch_size) { continue 2; // Load all the entities of this batch that are tracked through their // initial revision in a single storage call. $initial_revision_entities = []; if ($initial_entity_ids = array_intersect_key($initial_revision_ids, $associated_revisions)) { $initial_revision_entities = $associated_entity_storage->loadMultiple($initial_entity_ids); } foreach (array_keys($associated_revisions) as $revision_id) { // If the workspace is tracking the entity's default revision (i.e. // the entity was created inside that workspace), we need to delete // the whole entity after all of its pending revisions are gone. if (isset($initial_revision_ids[$revision_id])) { $associated_entity_storage->delete([$associated_entity_storage->load($initial_revision_ids[$revision_id])]); /** @var \Drupal\Core\Entity\RevisionableInterface&\Drupal\Core\Entity\EntityPublishedInterface $entity */ $entity = $initial_revision_entities[$initial_revision_ids[$revision_id]] ?? NULL; if (!$entity) { continue; } // The entity might have gone live after it was created in this // workspace. This can happen when the entity is edited in multiple // workspaces at the same time and one of the others is published. // An entity with a published default revision is live content, so // we only delete the revision created in this workspace, not the // whole entity. if ($entity->isPublished()) { if ($entity->getRevisionId() != $revision_id) { $associated_entity_storage->deleteRevision($revision_id); } else { // The default revision of an entity can not be deleted, so we // only remove its tracking records. $this->workspaceTracker->deleteTrackedEntities($workspace_id, $entity_type_id, [$entity->id()], [$revision_id]); } } else { $associated_entity_storage->delete([$entity]); } } else { // Delete the associated entity revision. Loading
core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php +59 −0 Original line number Diff line number Diff line Loading @@ -280,6 +280,65 @@ public function testDeletingPublishedWorkspace(): void { $this->assertTrue($revisions[3]->isDefaultRevision()); $this->assertFalse(isset($revisions[4])); $this->assertFalse(isset($revisions[5])); // Check that deleting a workspace does not delete the entities which were // created in it but became live content in the meantime. $node_storage = $this->entityTypeManager->getStorage('node'); /** @var \Drupal\workspaces\WorkspaceTrackerInterface $workspace_tracker */ $workspace_tracker = \Drupal::service('workspaces.tracker'); $workspace_3 = Workspace::create(['id' => 'summer', 'label' => 'Summer']); $workspace_3->save(); $workspace_4 = Workspace::create(['id' => 'winter', 'label' => 'Winter']); $workspace_4->save(); // Create two new nodes in the 'summer' workspace. The published one gets an // unpublished initial revision (6) and a published pending revision (7), // while the unpublished one only gets an initial revision (8). $this->workspaceManager->setActiveWorkspace($workspace_3); $node_1 = $this->createNode(['status' => TRUE]); $node_2 = $this->createNode(['status' => FALSE]); // Simulate a module that bypasses the entity editing protections and // creates a new revision (9) of the first node in another workspace. $this->workspaceManager->setActiveWorkspace($workspace_4); $node_1 = $node_storage->load($node_1->id()); $node_1->setNewRevision(TRUE); $node_1->setPublished(); $node_1->save(); // Publishing the 'winter' workspace makes revision 9 the published default // revision of the first node. $workspace_4->publish(); // Publish the second node in Live without creating a new revision, so its // initial revision (8) is also its default revision. $this->workspaceManager->switchToLive(); $node_storage->resetCache(); $node_2 = $node_storage->load($node_2->id()); $node_2->setPublished(); $node_2->save(); // Deleting the 'summer' workspace has to purge only the revisions created // in it, because both nodes are live content now. $workspace_3->delete(); $node_storage->resetCache(); $node_1 = $node_storage->load($node_1->id()); $this->assertNotNull($node_1); $this->assertTrue($node_1->isPublished()); $this->assertEquals(9, $node_1->getRevisionId()); $this->assertEmpty($node_storage->loadMultipleRevisions([6, 7])); // The initial revision of the second node is also its default revision, so // it can not be deleted. Only its tracking records are removed. $node_2 = $node_storage->load($node_2->id()); $this->assertNotNull($node_2); $this->assertTrue($node_2->isPublished()); $this->assertEquals(8, $node_2->getRevisionId()); $this->assertEmpty($workspace_tracker->getAllTrackedRevisions($workspace_3->id(), 'node')); // The purge finished, so the workspace ID is no longer in the queue. $this->assertEmpty(\Drupal::state()->get('workspace.deleted')); } /** Loading