From 775b3cffe5849975dabfc4c4063cd7cfd61be0a1 Mon Sep 17 00:00:00 2001 From: catch Date: Mon, 13 Feb 2023 10:01:47 +0000 Subject: [PATCH] Issue #3181439 by tstoeckler, Gauravvv, Tomefa, Sam152, smustgrave, larowlan: Content Moderation fatals when a moderated entity is re-saved on hook_insert() --- .../src/Entity/ContentModerationState.php | 7 +- .../content_moderation_test_resave.info.yml | 7 ++ .../content_moderation_test_resave.install | 15 +++ .../content_moderation_test_resave.module | 30 +++++ .../Kernel/ContentModerationResaveTest.php | 107 ++++++++++++++++++ 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.info.yml create mode 100644 core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.install create mode 100644 core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module create mode 100644 core/modules/content_moderation/tests/src/Kernel/ContentModerationResaveTest.php diff --git a/core/modules/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php index 6d0cb0abd57..fabce0090f5 100644 --- a/core/modules/content_moderation/src/Entity/ContentModerationState.php +++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php @@ -134,12 +134,17 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $storage = \Drupal::entityTypeManager()->getStorage('content_moderation_state'); + // New entities may not have a loaded revision ID at this point, but the + // creation of a content moderation state entity may have already been + // triggered elsewhere. In this case we have to match on the revision ID + // (instead of the loaded revision ID). + $revision_id = $entity->getLoadedRevisionId() ?: $entity->getRevisionId(); $ids = $storage->getQuery() ->accessCheck(FALSE) ->condition('content_entity_type_id', $entity->getEntityTypeId()) ->condition('content_entity_id', $entity->id()) ->condition('workflow', $moderation_info->getWorkflowForEntity($entity)->id()) - ->condition('content_entity_revision_id', $entity->getLoadedRevisionId()) + ->condition('content_entity_revision_id', $revision_id) ->allRevisions() ->execute(); diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.info.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.info.yml new file mode 100644 index 00000000000..25d8618bbf4 --- /dev/null +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.info.yml @@ -0,0 +1,7 @@ +name: 'Content moderation test re-save' +type: module +description: 'Re-saves moderated entities for testing purposes.' +package: Testing +version: VERSION +dependencies: + - drupal:content_moderation diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.install b/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.install new file mode 100644 index 00000000000..383e59ee8c2 --- /dev/null +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.install @@ -0,0 +1,15 @@ +isModeratedEntity($entity)) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + // Saving the passed entity object would populate its loaded revision ID, + // which we want to avoid. Thus, save a clone of the original object. + $cloned_entity = clone $entity; + // Set the entity's syncing status, as we do not want Content Moderation to + // create new revisions for the re-saving. Without this call Content + // Moderation would end up creating two separate content moderation state + // entities: one for the re-save revision and one for the initial revision. + $cloned_entity->setSyncing(TRUE)->save(); + + // Record the fact that a re-save happened. + \Drupal::state()->set('content_moderation_test_resave', TRUE); + } +} diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationResaveTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationResaveTest.php new file mode 100644 index 00000000000..15f9aaf03a5 --- /dev/null +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationResaveTest.php @@ -0,0 +1,107 @@ +installEntitySchema('content_moderation_state'); + $this->installEntitySchema($entity_type_id); + + $workflow = $this->createEditorialWorkflow(); + $this->addEntityTypeAndBundleToWorkflow($workflow, $entity_type_id, $entity_type_id); + + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ + $entity_type_manager = $this->container->get('entity_type.manager'); + $this->contentModerationStateStorage = $entity_type_manager->getStorage('content_moderation_state'); + $this->entityStorage = $entity_type_manager->getStorage($entity_type_id); + $this->state = $this->container->get('state'); + } + + /** + * Tests that Content Moderation works with entities being resaved. + */ + public function testContentModerationResave() { + $entity = $this->entityStorage->create(); + $this->assertSame('draft', $entity->get('moderation_state')->value); + $this->assertNull(\Drupal::state()->get('content_moderation_test_resave')); + $this->assertNull(ContentModerationState::loadFromModeratedEntity($entity)); + $content_moderation_state_query = $this->contentModerationStateStorage + ->getQuery() + ->accessCheck(FALSE) + ->count(); + $this->assertSame(0, (int) $content_moderation_state_query->execute()); + $content_moderation_state_revision_query = $this->contentModerationStateStorage + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->count(); + $this->assertSame(0, (int) $content_moderation_state_revision_query->execute()); + + // The test module will re-save the entity in its hook_insert() + // implementation creating the content moderation state entity before + // Content Moderation's hook_insert() has run for the initial save + // operation. + $entity->save(); + $this->assertSame('draft', $entity->get('moderation_state')->value); + $this->assertTrue(\Drupal::state()->get('content_moderation_test_resave')); + $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); + $this->assertInstanceOf(ContentModerationState::class, $content_moderation_state); + $this->assertSame(1, (int) $content_moderation_state_query->execute()); + $this->assertSame(1, (int) $content_moderation_state_revision_query->execute()); + } + +}