diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php index 8a33c76ed8a8..0d4c43a33343 100644 --- a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php +++ b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php @@ -127,7 +127,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact $transitions = $this->validator->getValidTransitions($entity, $this->currentUser); $transition_labels = []; - $default_value = NULL; + $default_value = $items->value; foreach ($transitions as $transition) { $transition_to_state = $transition->to(); $transition_labels[$transition_to_state->id()] = $transition_to_state->label(); diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php index c33ccacdeec1..64c5ad849796 100644 --- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php @@ -16,7 +16,6 @@ use Drupal\Core\TypedData\ComputedItemListTrait; class ModerationStateFieldItemList extends FieldItemList { use ComputedItemListTrait { - ensureComputedValue as traitEnsureComputedValue; get as traitGet; } @@ -34,19 +33,6 @@ class ModerationStateFieldItemList extends FieldItemList { } } - /** - * {@inheritdoc} - */ - protected function ensureComputedValue() { - // If the moderation state field is set to an empty value, always recompute - // the state. Empty is not a valid moderation state value, when none is - // present the default state is used. - if (!isset($this->list[0]) || $this->list[0]->isEmpty()) { - $this->valueComputed = FALSE; - } - $this->traitEnsureComputedValue(); - } - /** * Gets the moderation state ID linked to a content entity revision. * @@ -140,10 +126,8 @@ class ModerationStateFieldItemList extends FieldItemList { */ public function setValue($values, $notify = TRUE) { parent::setValue($values, $notify); + $this->valueComputed = TRUE; - if (isset($this->list[0])) { - $this->valueComputed = TRUE; - } // If the parent created a field item and if the parent should be notified // about the change (e.g. this is not initialized with the current value), // update the moderated entity. diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php index c3b9c815fe05..7894cadf752c 100644 --- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php +++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\content_moderation\ModerationInformationInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraint; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -89,6 +90,13 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements return; } + // If the entity is moderated and the item list is empty, ensure users see + // the same required message as typical NotNull constraints. + if ($value->isEmpty()) { + $this->context->addViolation((new NotNullConstraint())->message); + return; + } + $workflow = $this->moderationInformation->getWorkflowForEntity($entity); if (!$workflow->getTypePlugin()->hasState($entity->moderation_state->value)) { diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php index 844e77d93082..b33ac6af511c 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php @@ -103,20 +103,70 @@ class ModerationStateFieldItemListTest extends KernelTestBase { } /** - * Tests the computed field when it is unset or set to an empty value. + * Tests the item list when it is emptied and appended to. */ - public function testSetEmptyState() { + public function testEmptyStateAndAppend() { + // This test case mimics the lifecycle of an entity that is being patched in + // a rest resource. + $this->testNode->moderation_state->setValue([]); + $this->assertTrue($this->testNode->moderation_state->isEmpty()); + $this->assertEmptiedModerationFieldItemList(); + + $this->testNode->moderation_state->appendItem(); + $this->assertEquals(1, $this->testNode->moderation_state->count()); + $this->assertEquals(NULL, $this->testNode->moderation_state->value); + $this->assertEmptiedModerationFieldItemList(); + } + + /** + * Test an empty value assigned to the field item. + */ + public function testEmptyFieldItem() { $this->testNode->moderation_state->value = ''; - $this->assertEquals('draft', $this->testNode->moderation_state->value); + $this->assertEquals('', $this->testNode->moderation_state->value); + $this->assertEmptiedModerationFieldItemList(); + } + /** + * Test an empty value assigned to the field item list. + */ + public function testEmptyFieldItemList() { $this->testNode->moderation_state = ''; - $this->assertEquals('draft', $this->testNode->moderation_state->value); + $this->assertEquals('', $this->testNode->moderation_state->value); + $this->assertEmptiedModerationFieldItemList(); + } + /** + * Test the field item when it is unset. + */ + public function testUnsetItemList() { unset($this->testNode->moderation_state); - $this->assertEquals('draft', $this->testNode->moderation_state->value); + $this->assertEquals(NULL, $this->testNode->moderation_state->value); + $this->assertEmptiedModerationFieldItemList(); + } + /** + * Test the field item when it is assigned NULL. + */ + public function testAssignNullItemList() { $this->testNode->moderation_state = NULL; - $this->assertEquals('draft', $this->testNode->moderation_state->value); + $this->assertEquals(NULL, $this->testNode->moderation_state->value); + $this->assertEmptiedModerationFieldItemList(); + } + + /** + * Assert the set of expectations when the moderation state field is emptied. + */ + protected function assertEmptiedModerationFieldItemList() { + $this->assertTrue($this->testNode->moderation_state->isEmpty()); + // Test the empty value causes a violation in the entity. + $violations = $this->testNode->validate(); + $this->assertCount(1, $violations); + $this->assertEquals('This value should not be null.', $violations->get(0)->getMessage()); + // Test that incorrectly saving the entity regardless will not produce a + // change in the moderation state. + $this->testNode->save(); + $this->assertEquals('draft', Node::load($this->testNode->id())->moderation_state->value); } /** @@ -132,6 +182,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase { $unmoderated_node->moderation_state = NULL; $this->assertEquals(0, $unmoderated_node->moderation_state->count()); + $this->assertCount(0, $unmoderated_node->validate()); } /** diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonAnonTest.php new file mode 100644 index 000000000000..f28788ac8018 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonAnonTest.php @@ -0,0 +1,24 @@ +grantPermissionsToTestedRole(['use editorial transition publish', 'use editorial transition create_new_draft']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $entity = parent::createEntity(); + if (!$this->workflow) { + $this->workflow = $this->createEditorialWorkflow(); + } + $this->workflow->getTypePlugin()->addEntityTypeAndBundle($entity->getEntityTypeId(), $entity->bundle()); + $this->workflow->save(); + + return $entity; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return array_merge(parent::getExpectedNormalizedEntity(), [ + 'moderation_state' => [ + [ + 'value' => 'published', + ], + ], + 'vid' => [ + [ + 'value' => (int) $this->entity->getRevisionId(), + ], + ], + ]); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php new file mode 100644 index 000000000000..4b91d766c6f4 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php @@ -0,0 +1,34 @@ +markTestSkipped(); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php new file mode 100644 index 000000000000..a321e03cb25f --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php @@ -0,0 +1,44 @@ +markTestSkipped(); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php new file mode 100644 index 000000000000..2014a56ff43d --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php @@ -0,0 +1,39 @@ +markTestSkipped(); + } + +}