Commit ce12a206 authored by catch's avatar catch
Browse files

fix: #3611989 Workspace purge deletes entities that went live through another workspace

By: amateescu
By: plach
(cherry picked from commit 043283f6)
parent a5d3407d
Loading
Loading
Loading
Loading
Loading
+34 −5
Original line number Diff line number Diff line
@@ -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.
+59 −0
Original line number Diff line number Diff line
@@ -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'));
  }

  /**