diff --git a/core/modules/content_moderation/content_moderation.install b/core/modules/content_moderation/content_moderation.install new file mode 100644 index 0000000..9d25310 --- /dev/null +++ b/core/modules/content_moderation/content_moderation.install @@ -0,0 +1,14 @@ +createQueue(); +} diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module index 7345e93..24d8814 100644 --- a/core/modules/content_moderation/content_moderation.module +++ b/core/modules/content_moderation/content_moderation.module @@ -238,11 +238,17 @@ function content_moderation_entity_bundle_info_alter(&$bundles) { * Implements hook_ENTITY_TYPE_insert(). */ function content_moderation_workflow_insert(WorkflowInterface $entity) { + if ($entity->getTypePlugin()->getPluginId() !== 'content_moderation') { + return; + } // Clear bundle cache so workflow gets added or removed from the bundle // information. \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); // Clear field cache so extra field is added or removed. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + // Workflows should ensure all content the workflow was created for + // gets a default state. + \Drupal::service('content_moderation.default_state_manager')->createQueueItems($entity); } /** diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml index 904bc0d..26982ab 100644 --- a/core/modules/content_moderation/content_moderation.services.yml +++ b/core/modules/content_moderation/content_moderation.services.yml @@ -20,3 +20,6 @@ services: arguments: ['@database'] tags: - { name: backend_overridable } + content_moderation.default_state_manager: + class: Drupal\content_moderation\DefaultStateManager + arguments: ['@queue', '@entity_type.manager'] diff --git a/core/modules/content_moderation/src/DefaultStateManager.php b/core/modules/content_moderation/src/DefaultStateManager.php new file mode 100644 index 0000000..61d6b09 --- /dev/null +++ b/core/modules/content_moderation/src/DefaultStateManager.php @@ -0,0 +1,62 @@ +queue = $queue->get('content_moderation_state_change'); + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function createQueueItems(WorkflowInterface $workflow) { + /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $type_plugin */ + $type_plugin = $workflow->getTypePlugin(); + foreach ($type_plugin->getEntityTypes() as $entity_type_id) { + + $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); + $entity_definition = $this->entityTypeManager->getDefinition($entity_type_id); + + foreach ($type_plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) { + + $query = $entity_storage->getQuery(); + $query->condition($entity_definition->getKey('bundle'), $bundle_id); + + foreach ($query->execute() as $entity_id) { + $this->queue->createItem([ + 'entity_id' => $entity_id, + 'entity_type_id' => $entity_type_id, + ]); + } + } + } + } + +} diff --git a/core/modules/content_moderation/src/DefaultStateManagerInterface.php b/core/modules/content_moderation/src/DefaultStateManagerInterface.php new file mode 100644 index 0000000..a32db7b --- /dev/null +++ b/core/modules/content_moderation/src/DefaultStateManagerInterface.php @@ -0,0 +1,20 @@ +entityTypeManager = $entity_type_manager; + $this->moderationInformation = $moderation_info; + $this->contentModerationStateStorage = $content_moderation_state_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('content_moderation.moderation_information'), + $container->get('entity_type.manager')->getStorage('content_moderation_state') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($entity_data) { + if (!$this->entityTypeManager->hasDefinition($entity_data['entity_type_id']) || !$entity = $this->entityTypeManager->getStorage($entity_data['entity_type_id'])->load($entity_data['entity_id'])) { + return; + } + if (!$this->moderationInformation->isModeratedEntity($entity)) { + return; + } + $existing_state = $this->contentModerationStateStorage->loadByProperties([ + 'content_entity_id' => $entity->id(), + 'content_entity_type_id' => $entity->getEntityTypeId() + ]); + if ($existing_state) { + return; + } + + // If an entity has a published status, assign it a corresponding content + // moderation state, otherwise assign the default so that all content + // with a workflow have a corresponding content_moderation_state entity. + if ($entity instanceof EntityPublishedInterface) { + $entity->moderation_state->value = $entity->isPublished() ? ContentModeration::DEFAULT_PUBLISHED_STATE : ContentModeration::DEFAULT_UNPUBLISHED_STATE; + } + else { + $entity->moderation_state->value = ContentModeration::DEFAULT_UNPUBLISHED_STATE; + } + $entity->save(); + } + +} diff --git a/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php index 31f1c18..fd75ccb 100644 --- a/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php +++ b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php @@ -31,6 +31,16 @@ class ContentModeration extends WorkflowTypeBase implements ContainerFactoryPlug use StringTranslationTrait; /** + * The state content which is published will be assigned. + */ + const DEFAULT_PUBLISHED_STATE = 'published'; + + /** + * The state content which is unpublished will be assigned. + */ + const DEFAULT_UNPUBLISHED_STATE = 'draft'; + + /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface diff --git a/core/modules/content_moderation/tests/src/Kernel/DefaultStatesTest.php b/core/modules/content_moderation/tests/src/Kernel/DefaultStatesTest.php new file mode 100644 index 0000000..353ac5b --- /dev/null +++ b/core/modules/content_moderation/tests/src/Kernel/DefaultStatesTest.php @@ -0,0 +1,245 @@ +installConfig('content_moderation'); + $this->installSchema('node', 'node_access'); + + $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('content_moderation_state'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + + $this->entityTypeManager = $this->container->get('entity_type.manager'); + $this->cron = $this->container->get('cron'); + $this->queueWorkerManager = $this->container->get('plugin.manager.queue_worker'); + $this->defaultStateManager = $this->container->get('content_moderation.default_state_manager'); + } + + /** + * Test setting an initial state on entities. + * + * @dataProvider initialStateTestCases + */ + public function testInitialState($entity_type_id, $initial_publish_status, $resulting_state) { + $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); + + $bundle_id = $entity_type_id; + if ($bundle_entity_type_id = $this->entityTypeManager->getDefinition($entity_type_id)->getBundleEntityType()) { + $this->entityTypeManager->getStorage($bundle_entity_type_id)->create([ + $this->entityTypeManager->getDefinition($bundle_entity_type_id)->getKey('id') => 'example', + ])->save(); + $bundle_id = 'example'; + } + + $entity = $entity_storage->create([ + 'title' => 'Test title', + 'type' => $bundle_id, + ]); + if ($entity instanceof EntityPublishedInterface) { + if ($initial_publish_status) { + $entity->setPublished(); + } + else { + $entity->setUnpublished(); + } + } + $entity->save(); + + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id); + $workflow->save(); + + $this->cron->run(); + + $entity = $entity_storage->load($entity->id()); + $this->assertEquals($resulting_state, $entity->moderation_state->value); + + // Ensure a real content moderation state entity is created. + $state_entity = $this->entityTypeManager->getStorage('content_moderation_state')->loadByProperties([ + 'content_entity_type_id' => $entity_type_id, + 'content_entity_id' => $entity->id(), + ]); + $state_entity = array_shift($state_entity); + $this->assertEquals($resulting_state, $state_entity->moderation_state->value); + } + + /** + * Test cases for ::testInitialState(). + */ + public function initialStateTestCases() { + return [ + 'Published entity' => [ + 'node', + TRUE, + 'published', + ], + 'Unpublished entity' => [ + 'node', + FALSE, + 'draft', + ], + 'Entity (without EntityPublishedInterface)' => [ + 'entity_test_rev', + NULL, + 'draft', + ], + ]; + } + + /** + * Test items are never queued for defaults twice. + */ + public function testNeverQueuedTwice() { + NodeType::create([ + 'type' => 'example', + ])->save(); + $node = Node::create([ + 'type' => 'example', + 'title' => 'Example Node', + 'status' => NodeInterface::NOT_PUBLISHED, + ]); + $node->save(); + + // Create a content moderation state entity for the test entity as to + // simlulate content moderation previously being enabled and applied to this + // content. + $content_moderation_state = ContentModerationState::create([ + 'workflow' => 'editorial', + 'moderation_state' => 'archived', + 'content_entity_type_id' => $node->getEntityTypeId(), + 'content_entity_id' => $node->id(), + 'content_entity_revision_id' => $node->getRevisionId(), + ]); + ContentModerationState::updateOrCreateFromEntity($content_moderation_state); + + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle($node->getEntityTypeId(), 'example'); + $workflow->save(); + + $node = Node::load($node->id()); + $this->assertEquals('archived', $node->moderation_state->value); + + $this->cron->run(); + + $node = Node::load($node->id()); + $this->assertEquals('archived', $node->moderation_state->value); + } + + /** + * Test the default state manager creates correct queue items. + */ + public function testCreateQueueItems() { + NodeType::create([ + 'type' => 'example', + ])->save(); + $node = Node::create([ + 'type' => 'example', + 'title' => 'Example Node', + 'status' => NodeInterface::NOT_PUBLISHED, + ]); + $node->save(); + + $moderation_queue = $this->container->get('queue')->get('content_moderation_state_change'); + + // Test queue items are not incorrectly created all entities when only + // a specific bundle is moderated. + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'not_example_bundle'); + $this->defaultStateManager->createQueueItems($workflow); + $this->assertEquals(0, $moderation_queue->numberOfItems()); + + // Assert items are correctly created for the moderated bundle. + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $this->defaultStateManager->createQueueItems($workflow); + $this->assertEquals(1, $moderation_queue->numberOfItems()); + } + + /** + * Test the queue worker silently removes invalid items from the queue. + */ + public function testQueueWorker() { + $worker = $this->queueWorkerManager->createInstance('content_moderation_state_change'); + // Invalid entity information returns silently to remove the invalid item + // from the queue. + $this->assertNull($worker->processItem([ + 'entity_type_id' => 'entity_test_rev', + 'entity_id' => 'foo', + ])); + $this->assertNull($worker->processItem([ + 'entity_type_id' => 'invalid_entity_type', + 'entity_id' => 'foo', + ])); + + // Test the queue worker on an item that is no longer under moderation. + $entity = EntityTestRev::create([]); + $entity->save(); + $this->assertNull($worker->processItem([ + 'entity_type_id' => $entity->getEntityTypeId(), + 'entity_id' => $entity->id(), + ])); + } + +}