Problem/Motivation

50-50 chance of this being a problem with Rest module or with Content Moderation, tagged against the former for now.

Updating the moderation_state field of an entity that is moderated by Content Moderation via Rest does not work.

Doing so yields:

Unprocessable Entity: validation failed.
moderation_state: Moderation state: this field cannot hold more than 1 values.

Note that I tried this with JSON, not with HAL, but I think denormalization should be the same for both.

The reason is that FieldableEntityNormalizerTrait::denormalizeFieldData() first removes any existing items via $field_item_list->setValue([]) and then \Drupal\serialization\Normalizer\FieldNormalizer::denormalize() adds a new item with the incoming data via $items->appendItem().

But because ModerationStateFieldItemList goes out of its way to never be empty the setValue() call does not actually lead to the item being removed. Then appendItem() will find the already existing item and gladly add another one.

Proposed resolution

?

Comments

tstoeckler created an issue. See original summary.

berdir’s picture

If ModerationStateFieldItemList does special stuff on removing a item maybe it should also override appendItem() so that things continues work as expected?

tstoeckler’s picture

Yes, I played around with that as well.

This little hack in ComputedItemListTrait::appendItem() fixed the issue for me. (It might break other fields, thus, "hack" ;-).)

   public function appendItem($value = NULL) {
-    $this->ensureComputedValue();
-    return parent::appendItem($value);
+    $item = parent::appendItem($value);
+    $this->valueComputed = TRUE;
+    return $item;
   }

But then I wasn't sure whether "the other side" of the equation isn't actually the problem. I.e. if there's already a field item and I call ::appendItem() isn't the expected outcome to have two field items? I'm not really sure about the semantics here...

wim leers’s picture

Title: Moderation state field cannot be updated via Rest » Moderation state field cannot be updated via REST, because special handling in ModerationStateFieldItemList
Component: rest.module » content_moderation.module
Issue tags: +Entity Field API

Per the discussion, it looks like this is more of a content_moderation problem than a rest problem. Or, to be more specific: the problem is with how it uses the Entity/Field API in a special way.

So moving there for now. I'm betting @amateescu will have thoughts on this :)

sam152’s picture

Status: Active » Needs review
StatusFileSize
new1.21 KB

We explicitly have test cases which assert we can't empty out the moderation_state field, ModerationStateFieldItemListTest::testSetEmptyState. This was a behavior of ModerationStateFieldItemList which predated ComputedItemListTrait, so I don't believe this is necessarily a problem with the trait. We had to override parts of it to keep that working.

For context, I believe there were some nasty implications down the call-stack if the moderation_state field was ever completely emptied (EntityFormDisplay attempts this when the moderation widget is hidden I think), hence the nuanced and unusual behavior.

I do believe there would be some implementation of FieldNormalizer::denormalize which doesn't rely on an empty FieldItemList which would satisfy regular FieldItemLists and the content_moderation one, but I don't know if we'd want to make such a concession for CM in an "upstream" subsystem. The alternative would be a custom normalizer. Testing this with the attached patch.

Whatever the implementation a full integration test using moderation_state with rest would probably be valuable. Rest has been a selling-point/consideration of a few implementation decisions along the way for CM, so not supporting it over something silly would be a shame. EntityResourceTestBase looks like possibly a good starting point?

Status: Needs review » Needs work

The last submitted patch, 5: 2943899_alt-field-normalizer-implementation-5.patch, failed testing. View results

sam152’s picture

Status: Needs work » Needs review
StatusFileSize
new1.22 KB

This should fix some test cases. This was failing because of deltas not being sequential in this test case:

    // Multi-value field: remove item 0. Then item 1 becomes item 0.
    $normalization_multi_value_tests = $this->getNormalizedPatchEntity();
    $normalization_multi_value_tests['field_rest_test_multivalue'] = $this->entity->get('field_rest_test_multivalue')->getValue();
    $normalization_remove_item = $normalization_multi_value_tests;
    unset($normalization_remove_item['field_rest_test_multivalue'][0]);

Re-keying the field values when using set instead of appendItem should fix a few things.

Status: Needs review » Needs work

The last submitted patch, 7: 2943899_alt-field-normalizer-implementation-6.patch, failed testing. View results

sam152’s picture

StatusFileSize
new8.17 KB

Stepping back from a solution, here is an approach to testing this. This adds the same suite of REST tests each entity type has for a moderated node. It currently fails with the same error reported in the issue summary:

{"message":"Unprocessable Entity: validation failed.\nmoderation_state: Moderation state: this field cannot hold more than 1 values.\n"}

sam152’s picture

Status: Needs work » Needs review
tstoeckler’s picture

Maybe this is a stupid question, but is there a reason not to add the test coverage in NodeResourceTestBase directly? I mean we already test e.g. the path field, as well, there which is also an "optional" field which only exists if the Path module is on. It seems similar to the moderation state field that gets added by Content Moderation. Thoughts?

sam152’s picture

I suppose one thing ModeratedNodeResourceTestBase could become in the future is a place to add more coverage around REST requests that interact with a moderated entity. The other aspect is: we'd essentially lose coverage for the Node entity under conditions where content_moderation is disabled. How important that is, I'm not sure.

sam152’s picture

I'm still not 100% on this approach, but I'll try fix the remaining tests so it can be properly evaluated. One strange semantic is: FieldItem classes will notify the parent FieldItemList of a change, even if that FieldItem is "detached" and not a member of the FieldItemList yet.

The last submitted patch, 9: 2943899_test-only-9.patch, failed testing. View results

The last submitted patch, 13: 2943899_test-only-13_TEST-ONLY.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 13: 2943899_test-only-13.patch, failed testing. View results

sam152’s picture

Status: Needs work » Needs review
StatusFileSize
new938 bytes
new11.4 KB
sam152’s picture

Another approach to fixing this might be to override ItemList::appendItem in ModerationStateFieldItemList to simply return the computed 0 delta, that feels a bit awkward too though.

Status: Needs review » Needs work

The last submitted patch, 17: 2943899_test-only-17.patch, failed testing. View results

amateescu’s picture

@Sam152, actually, that might be the correct thing to do since ModerationStateFieldItemList goes out of its way to always have a value at delta 0 :)

sam152’s picture

Realising that was mentioned all the way back in #2 :)

On the other hand: could removing appendItem in rest provide better support for fields which generally aren't emptyable, especially if the fix isn't really that ugly (this might be a uniquely CM thing)? Overriding a method in an already tricky/complex class to behave in opposition to its name and purpose seems like a bit of a "gotcha" that could bite us in the future.

amateescu’s picture

Overriding a method in an already tricky/complex class to behave in opposition to its name and purpose seems like a bit of a "gotcha" that could bite us in the future.

That's a very good point. Do you know if it's documented anywhere why exactly are we trying to always keep the moderation_state field not empty? Maybe this is just for historical reasons or because of missing core functionalities when the original code of CM was written, and then we could try to make it behave like every other field.

sam152’s picture

The unemptiableness of the field was discussed here in the context of preserving the behavior when migrating to ComputedItemListTrait. Here are the test cases that failed when ComputedItemListTrait implicitly removed that behaviour. We added an explicit test case for it in that issue:

  /**
   * Tests the computed field when it is unset or set to an empty value.
   */
  public function testSetEmptyState() {
    $this->testNode->moderation_state->value = '';
    $this->assertEquals('draft', $this->testNode->moderation_state->value);

    $this->testNode->moderation_state = '';
    $this->assertEquals('draft', $this->testNode->moderation_state->value);

    unset($this->testNode->moderation_state);
    $this->assertEquals('draft', $this->testNode->moderation_state->value);
  }

I'm not totally sure where else this has come up, I feel like it has popped up a few other times but I couldn't say where.

amateescu’s picture

Yep, I remember the recent discussion from the switch to ComputedItemListTrait, but I was wondering about the original/historical reason for that behavior.

The reason for questioning it is that it seems to cause a lot of problems so maybe we should address the root cause instead :)

sam152’s picture

I suppose the failing test cases can be a reference for what parts broke with an emptiable field. You're right, it might be worth evaluating the effort in addressing those issues instead.

amateescu’s picture

So if we go back to your comment from #2915398-38: The moderation_state field is not computed during the creation of a new entity translation. (first paragraph), we already have special handling in both setValue() and onChange() so we know that whatever change needs to happen to get rid of the "never empty value" behavior will require both of those methods to stay in sync.

I'm sorry that I didn't follow up on that path at the time, but I feel we should restart the investigation based on your "attempt" patch form #40 in that issue..

sam152’s picture

sam152’s picture

Status: Needs work » Needs review
StatusFileSize
new3.34 KB
new11.5 KB

Trying to approach suggested by @amateescu to make the moderation_state field emptiable. Lets see what the testbot says. This passes the new rest tests as expected.

Status: Needs review » Needs work

The last submitted patch, 28: 2943899-28.patch, failed testing. View results

sam152’s picture

The fail is what I mentioned in #5:

For context, I believe there were some nasty implications down the call-stack if the moderation_state field was ever completely emptied (EntityFormDisplay attempts this when the moderation widget is hidden I think), hence the nuanced and unusual behavior.

When the widget is hidden an empty string is assigned to moderation_state. Having a look at this now.

sam152’s picture

Status: Needs work » Needs review
StatusFileSize
new878 bytes
new12.36 KB

Lets try this: avoid ever making the ModerationStateWidget assign '' as a state, even if it's hidden by access control. Instead make the default the value already provided/computed on $items so WidgetBase::extractFormValues simply reassigns that same value.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

rahullamkhade’s picture

@Sam152

Patch #31 works but it's missing permissions while making rest call. That means draft creator can also change moderation state to published even though he do not have publish transition permission.

rahullamkhade’s picture

Patch #31 works!

But problem is user can update moderation state though REST even he/she do not have that transition permission. For eg: User with only create draft permission can update node via REST PATCH to published state event if do not have permission to publish content from draft state.

rahullamkhade’s picture

Version: 8.6.x-dev » 8.7.x-dev
sam152’s picture

StatusFileSize
new12.36 KB

Reuploading for another test run. Would be good to get this issue back on track.

amateescu’s picture

The patch looks great to me :) I think the only question I have is what happens when we try to save a node with an empty value for the moderation state field. I would expect that the new revision would keep the previous moderation state.

I see we added some assertions to check for assigning empty values, can we expand them a bit to also save the node and check that the moderation state didn't change?

Status: Needs review » Needs work

The last submitted patch, 36: 2943899-31.patch, failed testing. View results

sam152’s picture

Good point. I think the validation of the entity might fail under those scenarios, is that a good enough level of protection if saving the node goes haywire? In any case, will add these scenarios to the test case and see what happens.

sam152’s picture

Status: Needs work » Needs review
StatusFileSize
new5.57 KB
new0 bytes

Fixing the test cases and expanding on our new coverage based on #37.

sam152’s picture

StatusFileSize
new14.46 KB

Correct patch for #40.

The last submitted patch, 40: 2943899-40.patch, failed testing. View results

amateescu’s picture

+++ b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php
@@ -103,29 +104,69 @@ public function testGet() {
+    $violations = $this->testNode->validate();
+    $this->assertCount(1, $violations);
+    $this->assertEquals('State <em class="placeholder"></em> does not exist on <em class="placeholder">Editorial</em> workflow', $violations->get(0)->getMessage());

I'm not sure this is the correct violation that a user should receive. Wouldn't it be better to make the field required instead?

sam152’s picture

StatusFileSize
new4.21 KB
new3.09 KB
new15.9 KB

Thanks for the review.

I looked into making the field required (see first interdiff), this breaks down for non-moderated bundles and without #2935932: Add a FieldDefinition class for defining bundle fields in code. it wont be easy to override "required" for each bundle :)

I worked on a second approach for adding a nicer message to our existing validator which respects isModeratedEntity.

The last submitted patch, 44: interdiff-failed-attempt-1.patch, failed testing. View results

The last submitted patch, 44: interdiff-attempt-2.patch, failed testing. View results

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

I have to say that I would have preferred the first approach, but we can always do it after #2935932: Add a FieldDefinition class for defining bundle fields in code. lands, and the cleanup provided by this patch is worth getting in sooner :)

The last submitted patch, 31: 2943899-31.patch, failed testing. View results

wim leers’s picture

Per #44 and #47, posted #2935932-110: Add a FieldDefinition class for defining bundle fields in code. to ensure we don't forget to do the indeed much nicer #44.1!

sam152’s picture

Once we have a field definition and field storage definition, we would actually be able to turn the moderation state field into a bundle field, so the validation message would be even better for non-moderated entities, the message would indicate that the field doesn't exist at all.

  • catch committed 1e5459e on 8.7.x
    Issue #2943899 by Sam152, amateescu, tstoeckler: Moderation state field...
catch’s picture

Version: 8.7.x-dev » 8.6.x-dev
Status: Reviewed & tested by the community » Fixed

  • catch committed babf1e9 on 8.6.x
    Issue #2943899 by Sam152, amateescu, tstoeckler: Moderation state field...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.