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
?
| Comment | File | Size | Author |
|---|---|---|---|
| #44 | 2943899-44.patch | 15.9 KB | sam152 |
| #44 | interdiff-attempt-2.patch | 3.09 KB | sam152 |
| #44 | interdiff-failed-attempt-1.patch | 4.21 KB | sam152 |
| #41 | 2943899-40.patch | 14.46 KB | sam152 |
| #40 | 2943899-40.patch | 0 bytes | sam152 |
Comments
Comment #2
berdirIf ModerationStateFieldItemList does special stuff on removing a item maybe it should also override appendItem() so that things continues work as expected?
Comment #3
tstoecklerYes, 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" ;-).)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...Comment #4
wim leersPer the discussion, it looks like this is more of a
content_moderationproblem than arestproblem. 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 :)
Comment #5
sam152 commentedWe explicitly have test cases which assert we can't empty out the moderation_state field,
ModerationStateFieldItemListTest::testSetEmptyState. This was a behavior ofModerationStateFieldItemListwhich predatedComputedItemListTrait, 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 (
EntityFormDisplayattempts 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::denormalizewhich doesn't rely on an emptyFieldItemListwhich 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_statewith 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.EntityResourceTestBaselooks like possibly a good starting point?Comment #7
sam152 commentedThis should fix some test cases. This was failing because of deltas not being sequential in this test case:
Re-keying the field values when using
setinstead ofappendItemshould fix a few things.Comment #9
sam152 commentedStepping 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:
Comment #10
sam152 commentedComment #11
tstoecklerMaybe this is a stupid question, but is there a reason not to add the test coverage in
NodeResourceTestBasedirectly? 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?Comment #12
sam152 commentedI suppose one thing
ModeratedNodeResourceTestBasecould 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 wherecontent_moderationis disabled. How important that is, I'm not sure.Comment #13
sam152 commentedI'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:
FieldItemclasses will notify the parentFieldItemListof a change, even if thatFieldItemis "detached" and not a member of theFieldItemListyet.Comment #17
sam152 commentedComment #18
sam152 commentedAnother approach to fixing this might be to override
ItemList::appendIteminModerationStateFieldItemListto simply return the computed 0 delta, that feels a bit awkward too though.Comment #20
amateescu commented@Sam152, actually, that might be the correct thing to do since
ModerationStateFieldItemListgoes out of its way to always have a value at delta 0 :)Comment #21
sam152 commentedRealising that was mentioned all the way back in #2 :)
On the other hand: could removing
appendItemin 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.Comment #22
amateescu commentedThat'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.
Comment #23
sam152 commentedThe 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 whenComputedItemListTraitimplicitly removed that behaviour. We added an explicit test case for it in that issue: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.
Comment #24
amateescu commentedYep, 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 :)
Comment #25
sam152 commentedI 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.
Comment #26
amateescu commentedSo 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()andonChange()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..
Comment #27
sam152 commentedI stumbled another issue impacted by assigning empty values to
moderation_state(#2915384: Alternative moderation_state field widgets do not receive the same access/visibility treatment as the default ModerationStateWidget.), in this case I think the issue would also be helped if rolled back the decision to avoid assigning''. Given that issue is around the behavior of the field on non moderated entities, I also think #2915383: The moderation_state base field is added to all revisionable entity types even if they do not have moderation enabled is kind of tangentially related too.Comment #28
sam152 commentedTrying to approach suggested by @amateescu to make the
moderation_statefield emptiable. Lets see what the testbot says. This passes the new rest tests as expected.Comment #30
sam152 commentedThe fail is what I mentioned in #5:
When the widget is hidden an empty string is assigned to
moderation_state. Having a look at this now.Comment #31
sam152 commentedLets 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$itemssoWidgetBase::extractFormValuessimply reassigns that same value.Comment #33
rahullamkhade commented@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.
Comment #34
rahullamkhade commentedPatch #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.
Comment #35
rahullamkhade commentedComment #36
sam152 commentedReuploading for another test run. Would be good to get this issue back on track.
Comment #37
amateescu commentedThe 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?
Comment #39
sam152 commentedGood 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.
Comment #40
sam152 commentedFixing the test cases and expanding on our new coverage based on #37.
Comment #41
sam152 commentedCorrect patch for #40.
Comment #43
amateescu commentedI'm not sure this is the correct violation that a user should receive. Wouldn't it be better to make the field required instead?
Comment #44
sam152 commentedThanks 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.Comment #47
amateescu commentedI 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 :)
Comment #49
wim leersPer #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!
Comment #50
sam152 commentedOnce 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.
Comment #52
catch