Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
Steps to reproduce:
1. Enable content moderation module and enable default workflow for article content type (at admin/config/workflow/workflows/manage/editorial)
1. Create article with alias /hello
2. Create a new draft of that article with filling in /hello2 as the url alias
3. The article is immediately accessible at /hello2 and not at /hello
Proposed resolution
Probably #2336597: Convert path aliases to full featured entities.
Alternatively add validation to stop people changing that value unless it's the default revision as a stopgap?
Remaining tasks
User interface changes
API changes
Data model changes
Comment | File | Size | Author |
---|---|---|---|
#91 | interdiff.txt | 2.62 KB | amateescu |
#91 | 2856363-91.patch | 7.4 KB | amateescu |
#83 | 2856363-83.test.patch | 3.97 KB | plach |
#77 | 2856363-77.patch | 7.53 KB | amateescu |
Comments
Comment #2
catchBumping to critical for visibility since this is how we've been dealing with data integrity issues with experimental modules in general.
Comment #3
larowlanPretty sure menu link will be same problem
Comment #4
catchComment #5
cilefen CreditAttribution: cilefen commentedI discussed this issue with @xjm, @cottser, @catch, @alexpott and @lauriii. We agreed this should be critical for the reasons @catch gave in #2.
Comment #6
Sam152 CreditAttribution: Sam152 as a volunteer commentedI think it would be great if #2336597: Convert path aliases to full featured entities solved this so you could create multiple revisions of a path alias. Realistically though, that seems like it's going in the direction of a whole other experimental module, which will need to be written and will have its own timeline to get to stable status. Given this is a critical data integrity issue, it risks the stable status of content_moderation.
Would everyone be comfortable with adding the validation to prevent alias changes on forward revisions as a means to downgrade this to a Major perhaps, with the view to fix it once the linked issue is resolved?
Another off the cuff suggestion would be to create aliases for the latest-version route for all forward revisions where paths change, eg "/node/1/latest" => "/new-path" then switch them over once the latest revision is published, but I think implementing and maintaining something like that would look a lot worse than some simple validation, especially since it's a stop-gap measure that we want to solve properly.
Comment #7
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI was thinking about this problem yesterday and actually started to write a path_alias module for #2336597: Convert path aliases to full featured entities, but then I realized that
node/1/revision/2/view
is a perfectly valid system path, so we could solve this with much less effort than rewriting the path alias system by using the 'revision' entity url for draft revisions.It would look something like the patch attached. The final patch is going to take a bit more effort to complete so I'd like to get some opinions first if this is an approach worth pursuing :)
Comment #8
Sam152 CreditAttribution: Sam152 as a volunteer commentedSounds like a good option. One issue might be that the revision path doesn't exist for all entity types, hence the suggestion of using the "latest-version" route, which content_moderation adds.
Other than that, if this is easy to do, all we'd need to include in addition to roughly the approach in #7 is switch the revision alias over to the canonical one once that revision is published.
Sounds doable. +1 from me.
Comment #9
timmillwoodThis obviously depends on the entity having a "revision" URL. In core that's only Node (because only Node and BlockContent are revisionable, and BlockContent isn't rendered as a full page anyway), but in contrib or custom? So I guess the final patch will need a check that the URL exists, but what do we do if it doesn't?
Other than that, it sounds like a good plan.
Comment #10
alexpottThis only works if the path alias is changing when creating the future draft revision - otherwise the old alias is updated to point at a revision that's not the default revision.
Comment #11
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@alexpott, yes, I said that the patch is incomplete, it was just a PoC to show everyone what the idea is :)
Comment #12
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@Sam152:
The bug that needs to be fixed here can be reproduced without Content Moderation, it's a general problem with core's forward revision support so we can not rely on a route added by CM.
@timmillwood:
We most likely need to throw a "incomplete entity definition" exception if the entity type is revisionable, has a canonical "view" route but lacks a "view revision" one.
Comment #13
jhodgdonAnother point: This will only work also if changing which revision is the current/published revision triggers the revision and its fields to be saved (and hence the postSave() method to be called) on both the revision that used to be current and the one that is now becoming current. Does that happen?
Comment #14
Mile23Patch in #7 still applies. Re-uploading it here so CI can happen.
Needs a test, since there are instructions for reproducing the error in the IS.
Comment #15
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@Mile23, I specifically used the 'do no test' option for the patch in #7 so we don't waste CI resources on a patch that was simply meant as a PoC and a discussion starting point.
Comment #16
Sam152 CreditAttribution: Sam152 as a volunteer commentedI like the plan thus far. My feeling is that it's worth pursuing a more fleshed out implementation + tests.
I think we'd need to silently fall-back to the leaky behaviour for BC reasons, but the rest of the plan seems like the best that has been suggested yet.
Comment #17
timmillwoodI've been testing the patch from #7 this morning and there are two big issues.
$entity->isDefaultRevision()
returnsTRUE
when it should beFALSE
.$entity->toUrl('revision')->getInternalPath()
is returning the canonical path not the revision path.I think this is due to
\Drupal\path\Plugin\Field\FieldType\PathItem::postSave
being called before the entity has been saved.Maybe this code needs to move to hook_entity_create and hook_entity_update?
Comment #18
timmillwoodit seems like hook_entity_create and hook_entity_update are too early too, and hook_entity_insert is too late because by then
$entity->path->alias
is empty.Comment #19
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedWe have to work with the assumption that #2846554: Make the PathItem field type actually computed and auto-load stored aliases will be committed sometime in the near future, at which point
$entity->path->alias
will have a value.I'm pretty sure that can not happen :)
Comment #20
timmillwoodLooks like a contrib module I had installed was causing the issues I mentioned in #17 and #18.
So here's an updated patch with a test.
Not sure I'm 100% happy with the logic, and think this is the best solution when no alias is added, but will think on it.
Comment #22
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI was talking to @timmillwood about this issue and a flaw in my proposal came up: there is nothing that could link multiple sources from the {url_alias} table to a single entity.
One possible solution for that would be to add another column to the table,
entity_id
and maybe evenrevision_id
. How do others feel about that?Comment #23
timmillwoodThis new patch fixes the failing tests in #20.
I also think this addresses the critical issue, but it doesn't fix the general issue with URL aliases.
Currently the path field is only added by the Path module to Node and Taxonomy term entity types, as this issue only relates to forward revisions and Taxonomy terms are not revisionable, this issue only affects nodes.
If someone creates a custom entity which manually implements the path field then they would benefit from this patch (as long as they have a revision path). However if they create url aliases without using the path module then they'll still get this issue.
Comment #24
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@timmillwood, are you sure the changes to
PathItem::postSave()
from #20 and #23 are actually needed? Maybe the new test coverage exposed some problems with the code from #7?This implies that *every* non-default revision should have an alias, but that's not the case.
We need an OR condition which falls back to the
$entity->toUrl()->getInternalPath()
source here :)The test should have code comments to explain exactly what it's testing.
And we should also test deleting revisions and whole entities as well.
Comment #25
timmillwood@amateescu - As we discussed the changed between #7 and #23 were needed to the forward alias doesn't overwrite the default alias.
#24.1 - I have tried to add a workaround for this, but not too happy with it.
#24.2 - added comments.
Also updated the test / patch to fix a new issue I thought about. What if you create a default revision, then don't change the alias in the forward revision, you'll get two aliases the same, but with different sources. I am now loading the alias for the canonical path, and not creating a new one if one already exists.
When discussing this with @amateescu he pointed me to #2846554: Make the PathItem field type actually computed and auto-load stored aliases, so I wonder if it's worth postponing on that?
Comment #27
timmillwoodTest didn't pass because of the widget.
Comment #28
dixon_This issue was discussed during the Hard Problems meeting at DrupalCon in Baltimore. See notes and decisions here: https://docs.google.com/document/d/1-5B-Gndhklns20NZpGQul10525rFWuYB2NbT...
Comment #29
timmillwoodFrom the Workflow Initiative meeting last week (see #28) the action items included "Quick solution should move forward", which is this issue.
Comment #30
timmillwoodThis patch fixes #24.1 properly.
It also tests the widget returns the correct default value each time we load the edit form.
Then extends the test to test deleting an whole entity as well as deleting a single revision.
Deleting a whole entity resulted in the following change
so we could delete all revision aliases with a source such as '/node/1/revisions/%'. I'm not really happy with this change.
Also deleting a single revision isn't working because
\Drupal\path\Plugin\Field\FieldType\PathFieldItemList::delete
is only called when deleting a whole entity. I'm wondering if this could be considered a core bug.Comment #32
timmillwoodThis duplicates some code, but fixes the issue of aliases still existing after deleting a revision.
Comment #33
timmillwoodLooking at how #2846554: Make the PathItem field type actually computed and auto-load stored aliases might've changed things, and re-running tests for that reason too.
Comment #35
timmillwoodRe-roll plus updates based on #2846554: Make the PathItem field type actually computed and auto-load stored aliases.
Comment #36
vijaycs85I did a manual testing and working as expected. We have revision with it's own path. Not sure if this is exactly same as proposed solution in issue summary. Here is some code review comments(mostly optional improvements).
using "LIKE" without excapeLike?
Looks like same code repeated. Is it worth having a helper?
Can we have a comment here on what are we checking?
This is the typical condition to check if we are in non-default revision on a revisionable entity. Worth having it as a API in entity level(i.e. allows to check any additional checks in future in one location)?
Missing docblock.
Comment #37
timmillwood#36.1 - yes, LIKE without escapseLike() is correct. escapleLike removes all '%' and '_' characters, and we need them now. See \Drupal\Core\Database\Connection::escapeLike
#36.2 - I had considered this and wasn't sure a service for this would add much, and a helper function is a definite no. I'll give it more thought.
#36.3 - Added
#36.4 - Maybe we can do that in a followup? But not really convinced of the gain.
#36.5 - Added
Comment #38
timmillwoodWhen thinking about #36.4 I decided that we need to depend on an entity type having a revision link template, not just being revisionable.
For #36.2 I wondered if we could extend
AliasStorage
, to create something likeEntityAliasStorage
, however I don't think this is feasible. We could add a another service, but I'm not sure it's needed at this stage.Comment #39
timmillwoodPath module only works with Node, Taxonomy Term, and Media. Taxonomy Term is not revisionable yet, but Media is and doesn't have a revision link template. So will regress back to the functionality before this patch until it gets one. #2885486: Media entity is revisionable but doesn't have a revision link template is a follow up for this. Not making it a requirement as Media is experimental.
Comment #40
catchThe IN query won't be case insensitive on postgres, which suggests missing test coverage here. Also supporting the workaround requiring this change makes me feel even more we should do the absolute minimal fix here. Either way shouldn't it be converted to multiple OR with LIKE instead of IN if we take this approach?
Why is this no longer necessary here, but still necessary above?
Comment #41
timmillwood#40.1 - I think an OR query could work. All we're trying to do here is simplify the code. We could so something like:
#40.2 - This allows us to do real LIKE conditions with values such as '/node/1/revision/%'.
Comment #42
timmillwoodI hope this addresses the concerns in #40.
Comment #44
pk188 CreditAttribution: pk188 at OpenSense Labs commentedComment #45
timmillwoodHere's an interdiff for #44.
Comment #46
timmillwoodThe patch in #44 is incorrect because this test is testing what happens when no path aliases is added and the field is left empty.
Comment #47
timmillwoodFixing #42 and #44.
Comment #48
catchNot sure my feedback on the revision path alias handling made it to this issue, must have been on a triage call or in irc or something, but I'm still not keen on the complexity it's adding here. Feels like a workaround when we could get the validation quick fix in for the critical + refactor path aliases fully later on.
Comment #49
timmillwood@catch - I think this is a better fix than the validation, and isn't too disruptive. At least it gives a workable solution, where as the validation and the solution in #2766957: Forward revisions + translation UI can result in forked draft revisions prevent people from doing what they want.
Also, this patch is written, writing the validation quick fix would take more time, which would could be spending on the final solution or other issues.
Comment #50
timodwhit CreditAttribution: timodwhit commentedComment #51
timodwhit CreditAttribution: timodwhit commentedWould this make paths fully revisionable? Does the patch here also support revert-ing a revision and preserving the audit trail?
It looks like it does but just want to verify.
Comment #52
timmillwoodI wouldn't go as far as saying it makes paths fully revisionable, but it allows you to have aliases on non-default revisions. I've not tested reverting so not 100% what happens.
Either way, this is much better than what we currently have, and better than preventing users from creating "draft" aliases.
Comment #53
timodwhit CreditAttribution: timodwhit commentedThanks @timmillwood
2 questions:
- Is the proposed data structure agreed upon? As in, will the revision path be the storage of the alias? I know there has been back and forth on the approach in the issue but would be good to have consensus as @timmillwood mentioned.
- If that is agreed upon, would it be okay to back port and post a patch for 8.3.x? This will help those looking to solve this issue in the short term until 8.4 is released. However, this is a little more complicated as ensureLoaded doesn't exist in 8.3 and I believe #2846554 is the source of that. Also, I do not want to create noise for this issue with, just want to give people a bandaid
Comment #54
timmillwoodI can't see this being back ported to 8.3, although you could backport it yourself and just patch 8.3 sites you run, this is done in many issues, where people share the 8.3 version of a patch.
Comment #55
timodwhit CreditAttribution: timodwhit commented@timmillwood, forsure!
With that though, for the sake of data parity/upgradability, etc, how certain do we feel that the revision template will be will be the proper identifier? Not trying to force too much of a decision, I just know that there was a PoC, other discussion and that developed into this patch. So just want to verify and update the issue description to reflect path forward.
Comment #56
timodwhit CreditAttribution: timodwhit commented@catch, I don't believe validation will work as a stop gap because of things like scheduled updates, or if content moderation is forced / always revisioned, or for companies that have a strict publishing policy must be a draft before being published, etc.
Comment #57
catch@timodwhit it works as a stop gap because it stops the critical data integrity issue of changes leaking from drafts to the live site.
To do the things you want properly, we'd need #2336597: Convert path aliases to full featured entities and workspaces.
Comment #58
timodwhit CreditAttribution: timodwhit commented@catch, fully understand.
How would validation work if revisions are forced and the defaultRevision can't be edited? Or would the stop gap be revisions have to be toggable for the leaks to stop?
Comment #59
catchYou wouldn't be able to change the path alias. Someone that can edit the default revision directly would be able to.
Marking CNW for test coverage for reverting, and defining what the behaviour should actually be (although again, I think it's better to attack that with workspaces and not spend the effort here).
Comment #60
timmillwoodI have just been testing out reverting and this patch doesn't change the current state of reverting content with path aliases in HEAD, which I think it (maybe unrelatedly) broken.
Steps to reproduce (with or without Content Moderation enabled):
Comment #61
catchI think that's 'by design' at the moment - path aliases are not in any way linked to the node itself except via the form, and aren't versioned at all. You can add a path alias for a node via the path UI, in the same way the book UI isn't versioned either.
What concerns me here is that this lack-of-relation is being further obscured by integrating with revision paths, whereas if we waited for workspaces, the idea of publishing or reverting an entire workspace would include path aliases, but without tying them to changes to nodes (for example it would include changes made via the path admin UI too).
Comment #62
timmillwoodTaking this patch in the same direction as #2858434: Menu changes from node form leak into live site when creating draft revision and #2858431: Book storage and UI is not revision aware, changes to drafts leak into live site by throwing an error message on forward revisions, and preventing the URL alias from being updated.
Comment #63
timodwhit CreditAttribution: timodwhit commentedSorry if I'm not reading the patch right, but I don't think this will work in some cases and I think those cases are enough to justify thinking through this more.
If you have a workflow of:
- Draft/Published -> Draft
- Draft ->Published
- Published -> Archived
- Archived -> Published
You cannot edit the default revision.
Further, how would something like pathauto work in this? Can the title never change if the default revision isn't being edited and the path created off of node:title?
Comment #64
timodwhit CreditAttribution: timodwhit commentedSo just tried the patch on simplytest.me, looks as though you can save the node and are presented with an error.
This could be confusing but it doesn't stop the editing. Sorry for the confusion
Comment #65
timmillwoodI'm not sure I understand #63, but as you say in #64, it does work. To stop it being confusing it's vital we get the wording correct.
Pathauto could be an interesting one, but here we wouldn't be altering what Pathauto does or how it works, so it would carry on as it is.
Comment #66
timmillwoodI thought a screenshot might be useful here.
Comment #67
cilefen CreditAttribution: cilefen commentedFrom a usability perspective: What if the content author does not remember the original alias?
Comment #68
timmillwoodThe original alias doesn't get lost / changed.
The only "lost" input is the alias they just entered before saving.
Comment #69
cilefen CreditAttribution: cilefen commentedOk, good.
Comment #70
Bojhan CreditAttribution: Bojhan as a volunteer and commentedReviewed this with Roy, Gabor, Adam Hoenich and Andrei.
Thanks for raising this, this is quite tricky - but actually solvable. While the ideal solution involves actually raising a validation error and recovery upon submission, we thought given the engineering, and timespan it will effect the user (hopefully resolved with workspaces) and number of users it will affect that this is not worth it.
Therefor the message is for us, a good intermediate solution to continue with.
What the message should contain:
Comment #71
dawehnerI@m highly confused about this code change. I would have expected something which works on the field widget level, not being part of the storage API.
Comment #72
timmillwood@dawehner - I think doing this in the widget might be too early, see #2883868: Content Moderation decides to set a new revision as the default one way too late in the entity update process.
Comment #73
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@Bojhan, I implemented the proposed changes from the call in this issue: #2858431-51: Book storage and UI is not revision aware, changes to drafts leak into live site if you want to take a look :)
Comment #74
plachThis is marked as MUST-HAVE in the roadmap.
Comment #75
dawehner"Might be", this is quite a vague reason :)
If I would work on this patch I would try to write a constrain validator, as this place allows you to also provide a human readable error message.
Comment #76
plach#2883868: Content Moderation decides to set a new revision as the default one way too late in the entity update process was merged, this can now be properly "fixed" with a validation error.
Comment #77
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedHere's the patch with a new constraint validator. And this is how it looks like, just like a regular entity form validation error:
Comment #78
dawehnerI try to understand what this means in the context of pathauto, which changes path aliases automatically. Given that pathauto should change its values just for published entities as well, right?
I wonder whether we can find a better name, but I cannot come up with a good short name to be honest.
Isn't the right thing to do to use
$entity->getLoadedRevisionId()
to load the original?Comment #79
plachLooks great and works as advertised, attaching a test-only patch.
I'm wondering whether it would make sense to use $entity->original if it is available.
I think we need the "actual" default revision here: we don't want the user to change the alias wrt what's actually published, I think, even if it's different from the value in the revision being the default one when the form was instantiated.
Comment #81
dawehnerOh you are totally right here!
Comment #82
plachFailed as expected, patch to review is #77 (green!)
Comment #83
plachOops, #78 was empty :(
Comment #84
timmillwoodI think
$entity->original
should work, but loadUnchanged() might give a more predictable result.Forward revision? draft revision? pending version? :D
Comment #86
plachFailed as expected for reals now, patch to review is still #77.
Comment #87
plachComment #88
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI also tried to figure out a better name but nothing came up..
Well, it doesn't, I tried :)
$entity->original
is not set at that point.Comment #89
plachLet's get this in :)
Patch to commit is #77
Comment #90
yoroy CreditAttribution: yoroy at Roy Scholten commentedIn light of #2875154: Clarify and document wording around drafts, revisions, published & friends, should we change all uses of "revision" to "version" in the user facing text? The screenshot in #77 uses both in a single message :)
Comment #91
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@yoroy, that's an excellent point! Now that the patch actually stops the form from being submitted I think we can also simplify the message a bit and drop the first sentence.
Comment #92
tstoecklerThis is the only line in the constraint validator that is
PathItem
-specific.I'm fine with deferring this to a follow-up, but I think we should be able to make this completely generic by using
FieldItemListInterface::equals()
, i.e. with the following:Not sure if that would actually be useful to the related book or menu patches.
That said - again - I don't want to unnecessarily hold this up, as it would be important to get this one in, so would be fine with tackling that in a follow-up. But thought it would be useful to bring up since we have all the smart folks here in the issue ;-)
Comment #94
catchComment #95
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@tstoeckler, that's how I approached the problem initially, but book and menu_ui do not provide fields at all, they're doing old-style form alters so there's nothing really reusable here.
Comment #97
Naikalongus CreditAttribution: Naikalongus as a volunteer and commentedComment #98
Naikalongus CreditAttribution: Naikalongus as a volunteer and commentedComment #99
cilefen CreditAttribution: cilefen commented