From 1e5459ebf2b84a15bdce3ae7af4f1f001072f3d0 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Thu, 1 Nov 2018 14:23:04 +0000 Subject: [PATCH] Issue #2943899 by Sam152, amateescu, tstoeckler: Moderation state field cannot be updated via REST, because special handling in ModerationStateFieldItemList --- .../FieldWidget/ModerationStateWidget.php | 2 +- .../Field/ModerationStateFieldItemList.php | 18 +---- .../ModerationStateConstraintValidator.php | 8 ++ .../ModerationStateFieldItemListTest.php | 63 ++++++++++++++-- .../ModeratedNodeJsonAnonTest.php | 24 ++++++ .../ModeratedNodeJsonBasicAuthTest.php | 34 +++++++++ .../ModeratedNodeJsonCookieTest.php | 29 ++++++++ .../ModeratedNodeResourceTestBase.php | 74 +++++++++++++++++++ .../ModeratedNodeXmlAnonTest.php | 34 +++++++++ .../ModeratedNodeXmlBasicAuthTest.php | 44 +++++++++++ .../ModeratedNodeXmlCookieTest.php | 39 ++++++++++ 11 files changed, 345 insertions(+), 24 deletions(-) create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonAnonTest.php create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonBasicAuthTest.php create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonCookieTest.php create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeResourceTestBase.php create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlAnonTest.php create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlBasicAuthTest.php create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeXmlCookieTest.php 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 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $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 @@ class ModerationStateFieldItemList extends FieldItemList { use ComputedItemListTrait { - ensureComputedValue as traitEnsureComputedValue; get as traitGet; } @@ -34,19 +33,6 @@ protected function computeValue() { } } - /** - * {@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 @@ public function onChange($delta) { */ 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\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 @@ public function validate($value, Constraint $constraint) { 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 @@ public function testGet() { } /** - * 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 @@ public function testNonModeratedEntity() { $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 @@ +<?php + +namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode; + +use Drupal\Tests\rest\Functional\AnonResourceTestTrait; + +/** + * @group rest + */ +class ModeratedNodeJsonAnonTest extends ModeratedNodeResourceTestBase { + + use AnonResourceTestTrait; + + /** + * {@inheritdoc} + */ + protected static $format = 'json'; + + /** + * {@inheritdoc} + */ + protected static $mimeType = 'application/json'; + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonBasicAuthTest.php new file mode 100644 index 000000000000..b47a86f65ffb --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonBasicAuthTest.php @@ -0,0 +1,34 @@ +<?php + +namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode; + +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; + +/** + * @group rest + */ +class ModeratedNodeJsonBasicAuthTest extends ModeratedNodeResourceTestBase { + + use BasicAuthResourceTestTrait; + + /** + * {@inheritdoc} + */ + public static $modules = ['basic_auth']; + + /** + * {@inheritdoc} + */ + protected static $format = 'json'; + + /** + * {@inheritdoc} + */ + protected static $mimeType = 'application/json'; + + /** + * {@inheritdoc} + */ + protected static $auth = 'basic_auth'; + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonCookieTest.php b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonCookieTest.php new file mode 100644 index 000000000000..08fc4b5f4fb0 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeJsonCookieTest.php @@ -0,0 +1,29 @@ +<?php + +namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode; + +use Drupal\Tests\rest\Functional\CookieResourceTestTrait; + +/** + * @group rest + */ +class ModeratedNodeJsonCookieTest extends ModeratedNodeResourceTestBase { + + use CookieResourceTestTrait; + + /** + * {@inheritdoc} + */ + protected static $format = 'json'; + + /** + * {@inheritdoc} + */ + protected static $mimeType = 'application/json'; + + /** + * {@inheritdoc} + */ + protected static $auth = 'cookie'; + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeResourceTestBase.php new file mode 100644 index 000000000000..054259dbc561 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/ModeratedNode/ModeratedNodeResourceTestBase.php @@ -0,0 +1,74 @@ +<?php + +namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode; + +use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; +use Drupal\Tests\node\Functional\Rest\NodeResourceTestBase; + +/** + * Extend the Node resource test base and apply moderation to the entity. + */ +abstract class ModeratedNodeResourceTestBase extends NodeResourceTestBase { + + use ContentModerationTestTrait; + + /** + * {@inheritdoc} + */ + public static $modules = ['content_moderation']; + + /** + * The test editorial workflow. + * + * @var \Drupal\workflows\WorkflowInterface + */ + protected $workflow; + + /** + * {@inheritdoc} + */ + protected function setUpAuthorization($method) { + parent::setUpAuthorization($method); + + switch ($method) { + case 'POST': + case 'PATCH': + case 'DELETE': + $this->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 @@ +<?php + +namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode; + +use Drupal\Tests\rest\Functional\AnonResourceTestTrait; +use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; + +/** + * @group rest + */ +class ModeratedNodeXmlAnonTest extends ModeratedNodeResourceTestBase { + + use AnonResourceTestTrait; + use XmlEntityNormalizationQuirksTrait; + + /** + * {@inheritdoc} + */ + protected static $format = 'xml'; + + /** + * {@inheritdoc} + */ + protected static $mimeType = 'text/xml; charset=UTF-8'; + + /** + * {@inheritdoc} + */ + public function testPatchPath() { + // Deserialization of the XML format is not supported. + $this->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 @@ +<?php + +namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode; + +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; + +/** + * @group rest + */ +class ModeratedNodeXmlBasicAuthTest extends ModeratedNodeResourceTestBase { + + use BasicAuthResourceTestTrait; + use XmlEntityNormalizationQuirksTrait; + + /** + * {@inheritdoc} + */ + public static $modules = ['basic_auth']; + + /** + * {@inheritdoc} + */ + protected static $format = 'xml'; + + /** + * {@inheritdoc} + */ + protected static $mimeType = 'text/xml; charset=UTF-8'; + + /** + * {@inheritdoc} + */ + protected static $auth = 'basic_auth'; + + /** + * {@inheritdoc} + */ + public function testPatchPath() { + // Deserialization of the XML format is not supported. + $this->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 @@ +<?php + +namespace Drupal\Tests\rest\Functional\EntityResource\ModeratedNode; + +use Drupal\Tests\rest\Functional\CookieResourceTestTrait; +use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; + +/** + * @group rest + */ +class ModeratedNodeXmlCookieTest extends ModeratedNodeResourceTestBase { + + use CookieResourceTestTrait; + use XmlEntityNormalizationQuirksTrait; + + /** + * {@inheritdoc} + */ + protected static $format = 'xml'; + + /** + * {@inheritdoc} + */ + protected static $mimeType = 'text/xml; charset=UTF-8'; + + /** + * {@inheritdoc} + */ + protected static $auth = 'cookie'; + + /** + * {@inheritdoc} + */ + public function testPatchPath() { + // Deserialization of the XML format is not supported. + $this->markTestSkipped(); + } + +} -- GitLab