The poll module uses #limit_validation_errors for its add-more button, which is fine. However those button's submit handler poll_more_choices_submit() calls node_form_submit_build_node(), which updates $form_state['node'] with not-validated values. In turn those values end up in $node on rebuild. Still the changes have to be validated again before the node form can be submitted.
However that way un-validated values may be passed around in $node during form rebuild, treated by modules as a regular $node - what potentially can lead to diverse bad security implications.
Comment | File | Size | Author |
---|---|---|---|
#107 | entity_builder-cleanup-735800-107.patch | 43.62 KB | effulgentsia |
#104 | node_form.patch | 41.01 KB | fago |
#101 | node_form.patch | 41.28 KB | fago |
#98 | node_form.patch | 40.31 KB | fago |
#96 | node_form.patch | 37.78 KB | fago |
Comments
Comment #1
sunsubscribing + tagging
Comment #2
chx CreditAttribution: chx commentedI dunno, the presumption was that before the node is saved the whole node will be validated. #limit_validation_errors is not firing form level submit handlers.
Comment #3
fagoBut http://api.drupal.org/api/function/poll_more_choices_submit/7 does.
Comment #4
Frando CreditAttribution: Frando commentedYeah, this is one of the ugliest parts of Drupal IMO.
This code is there because node_form defines its master submit handler (the function that gets called once you click on the "Save" button) as a button-level submit handler and not as a form level submit handler. At the same time, other modules (in core e.g. menu) that form_alter the node form add their #submit handlers on the form level. For example, menu.module adds a form level #submit handler in which it sorts out its values in $form_state['values'].
Now, for multistep forms, these submit handlers need to fire so that upon rebuild, the pseudo-$node-object includes the current menu values.
So this is actually intended behavior.
However, I now think that this code is so ugly that we have to fix it for D7. Just removing the form_execute_handlers line in node_form_submit_build_node will break menu.module for non-javscript multistep flows.
What we need is a proper hook on a form that fires on *all* submit events of the node form, for both button level and form level submit handlers. This is what the ugly code above emulates. To do it properly, I propose the following:
A) Remove the #submit property on the node form's save button and instead let it fire as a form-level submit.
B) Remove that form_execute_handlers line from node_form_submit_build_node()
C) Add a hook_node_submit($form, $form_state) that fires in node_form_submit_build_node().
D) Move the code in menu.module from a #submit handler into menu_node_submit().
E1) Define (by convention) that all submit handlers on entity forms need to call the #builder_function.
Or E2) make FAPI call the #builder_function prior to any submit. This could actually work out and make #builder_function an "official" FAPI property instead of a hidden field api thing (can also be a follow up patch - currently all submit handlers already call #builder_function).
This is a big step towards #367006: [meta] Field attach API integration for entity forms is ungrokable, then. But by now I really think that this step is unavoidable.
Comment #5
Frando CreditAttribution: Frando commentedHere's a first patch that does what I outlined above. Tests will break, as locale and book modules still have to be updated in the same way as menu.
Comment #6
Frando CreditAttribution: Frando commentedI likely won't have time to drive this home, so if anyone wants to take over #5 go for it. Apart from fixing locale and book my point E2 above should also be feasable for D7 (let FAPI call #builder_function automatically for all submits, regardless of whether it's a button or form level submit).
The other clean alternative would IMO be a function entity_submit that gets called by all entity form submit handlers (field api add more, preview, poll add more etc). This function would call the entity's submit function (or reach out to a new submit() method in the entity controller, which would be cleaner) and invoke the hooks. Either the entity_submit function or the entity's submit callback has to cast the form values into an object (that is ugly, but we cannot change that in D7 I think because that's not as easy as it seems).
Comment #7
Frando CreditAttribution: Frando commentedthis is not specific to poll, moving and retitling
Comment #9
Frando CreditAttribution: Frando commentedTo reformulate what this patch does. We used to call the node form's form level submit functions implicitly on all submits, including button level submits. We need this kind of functionality, because in the way the node form currently works, modules need code that runs on all submits to ensure working multistep. Therefore, we make this behavior explicit by introducing a hook_node_submit(), and also align non-field modules more closely to what field API does.
API docs for hook_node_submit() will look like this:
Comment #10
Frando CreditAttribution: Frando commentedThis could count as an API change, as modules that form_altered #submit handlers onto the node form and relied upon these handlers being called for non-form-level submits would have to be updated.
Comment #11
fagoad #7:
Indeed the issue is in all buttons rebuilding the node form without validation - field API calls the #builder_function which in turn takes over the values in $node too.
However while I really like to clean that #submit mess up too, I don't see how it helps us for that particular problem? Still we would have to invoke hook_node_submit() instead and pass through the values to the next step to keep those. But the problem is that we want to keep the values although they aren't validated - so they may not go into $node.
Thus the multistep workflow of the form is broken, it's not capable and was never designed to work with limited validation. So the only easy way to solve that is imho:
* get #735808: fix multiple field value form to work with form API persistence in
* fix the node form to leverage the form API persistence mode
That way we don't have to call node #submit for simple rebuilds as the form API keeps the values for us.
@workflow in 4:
As yched explained in #367006-74: [meta] Field attach API integration for entity forms is ungrokable we can't fully untie entity validation from the form. So I see no point in having entity level validation at all. Instead I think we should bring validation back to the form, thus running on $form and $form_state. Still we can have a hook for that so modules can easily validate *any* form of those entity, but the entity object passed would be just the previous, unchanged one - what's being validated are the form values.
That way we can remove the #builder_function and just build the entity object on #submit and also invoke hook_entity_submit there. So we would also *not* break API compatibility as it's still possible to have your own #submit handler for that. Furthermore entity forms are working just as any regular form which makes a better developer experience.
The problem left is hook_node_validate() which passes a crippled $node object around as it does now. We could leave it with that and keep the (broken) API or just do an API change and fix it by doing a validation hook as described above.
Comment #12
yched CreditAttribution: yched commentedsubscribe
Comment #13
fagoHere is patch that implements the possible solution as described in #11. It requires the patch from #735808-3: fix multiple field value form to work with form API persistence to be applied.
Changes summarized:
* Relies on form API persistence mode for the node form and removes the own persistence handling. Thus there is no updated $node object passed around, so that the problem described in #1 is fixed.
* Makes the #builder_function optional as we don't need it any more.
* Keeps API compatibility for #submit handler but introduces a submit hook, so contribs can start migrating to that.
* Keeps node_validate() thus API compatibility for now, but places a @todo for D8.
* I left the update of $form_state['node'] in node_form_submit_build_node() but added a comment that the form must have been validated before this is called - that way $form_state['node'] will be updated on rebuilds running full validation as previous. However in case of limited validation it won't and may not be updated - thus the module doing limited validation has to take the form values into account during its rebuild.
Also I noted the book module currently runs node_form_submit_build_node(), but it doesn't need to. Next its #ajax callback doesn't do a regular form rebuild, thus that needs to be fixed. Anyway that should be a separate issue.
Comment #14
fagoNote: node_form_fix_all.patch also includes the patch from #735808: fix multiple field value form to work with form API persistence so the bot can test it.
Comment #16
effulgentsia CreditAttribution: effulgentsia commentedsubscribing
Comment #17
fagoHere is an updated patch fixing the poll module. Also I improved the logic of node_form() so that an updated $form_state['node'] is used during a rebuild as $node.
>thus the module doing limited validation has to take the form values into account during its rebuild.
Better we should do partial updates to $node in case of limited validation. That way modules don't have to obey the form values + $node, instead just $node can be used as usual. Also doing partial updates (= only updating what has been validated) makes imo much sense.
I fixed the poll module that way and also changed the book module to do a partial update of $node only.
Comment #18
Dries CreditAttribution: Dries commentedI just committed #735808: fix multiple field value form to work with form API persistence, by the way.
Comment #19
fagoGreat!
Now we are able to leverage form API persistence. Thus we can do partial updates of $node by only updating it with validated form values without loosing the other form values. This patch does so for the node form, so please review.
Comment #20
fagoFixed the form to properly rebuild in case of non-JS field add more submits.
Comment #21
yched CreditAttribution: yched commentedNot sure where this puts #629252: field_attach_form() should make available all field translations on submit
Comment #22
fago@yched: I replied there.
As we noted in #11 that the field add more functionality is affected of this bug too, so this bug affects not only node forms but all entity forms. Still I'd suggest to determine the right fix for the node form - our most complicated case - first, and then apply the fix to all other entity forms in a follow-up.
This makes me think whether we shouldn't remove the #builder_function at all as its use together with #limited_validation cannot be safe. This means that as a consequence the field API form demands the form API persistence layer to be there so other updated values aren't lost on rebuild - but I see no problem with that, as the form API persistence layer can be disabled only partially too as done in the patch for poll module rebuilds.
Comment #23
sunoh, I knew there was a related patch somewhere. Just had to deal with this node_form_submit_build_node() trickery also over in #757154: Base form_id via hook_forms() not taken into account for #validate, #submit, hook_form_FORMID_alter()
I guess we should get this patch in first. I like this patch, and especially the possibility to ditch #builder_function in a follow-up issue.
Needs a couple of clean-ups + thorough review though.
Comment #24
fagoYep, I do think this one is really important as to fix this issue we need to solve that fundamental problem of limited-validations-errors + manual rebuild using the #builder function not working together.
I think we should even completely remove #builder_function, as any module implementing it for the field API would ran into this issue with the field API rebuilds using limited validation. As a nice side-effect people don't have to wrap their head around #builder_function any more then.
Comment #25
effulgentsia CreditAttribution: effulgentsia commentedThis is a re-roll of #20 only. I don't yet understand this patch enough to offer constructive review, but seems like an important cleanup that will make other important cleanups possible, so I hope those of you who understand it keep driving it forward. If I ever grok it, I'll try to help move it along as well.
Comment #26
fagoWell I hope at least the problem is clear?
* When limited validation is enabled, the node form is rebuilt and not validated form values go into $node - which then modules use to construct the form. However $node is assumed to be safe, thus this is a potential security issue.
* To fix this, we can't take all the unvalidated values into $node. However to not loose the latest input values during a rebuild we need to leverage the form API persistence.
* But still I think the underlying problem is #763376: Not validated form values appear in $form_state - thus the values shouldn't be there at all. Anyway, this is the necessary fix to not use those values (whether they are there are not.)
Comment #27
Dries CreditAttribution: Dries commentedIn #23, sun mentioned that this needed an additional review -- not sure if he was volunteering? ;-)
Here is my first review:
Should we add a @todo stating that we want to remove this?
This reads a bit weird -- not sure I can make sense of that comment. Given that this is a D8 todo, let's be a bit more rigorous.
Same as above.
It would be nice to explain why we disable the form API persistence.
Comment #28
catchAgreed on the comments, also one of the new _rebuild() functions has no phpdoc. I haven't reviewed the patch seriously but this plus the builder function is extremely annoying to deal with in practice so agreed with the critical status. CNW for the additional clarifications.
Comment #29
fagoOh I completely missed that review, thanks. Given that fixing this requires all entity forms to change, we really should fix this asap.
ad #27:
I had a look at the mentioned comments and improved them. Hope they are clear now.
@#builder function:
As I noted in #24 the concept of the builder function doesn't play with the limited validation of the buttons used by the field API - as it's broken we should remove it even for D7. But as of now entity forms rely on it. So let's fix the node form, then apply the same pattern to the other entity forms an scratch the builder function. Thus I added the note to remove it *now*.
Comment #30
effulgentsia CreditAttribution: effulgentsia commentedThis is a cleanup of #29. I think it's ready to fly. Follow up issues will be needed (e.g., to fix the other entity forms, and to address some of Frando's goals in #4 that later comments decided to pull out of the scope of this issue), but I think this solves this particular issue.
Comment #31
effulgentsia CreditAttribution: effulgentsia commentedThis patch adds a comment explaining this ugliness.
New hook: this patch converts menu.module to use it.
This patch uses the converted menu.module as the example.
88 critical left. Go review some!
Comment #32
fagoeffulgentsia, you rock!
Your code comments are great, very precise, but easy to follow!
Also +1 (re)introducing hook_node_submit(). That's the right step to take now. Thus we can fully build upon that for d8, so the transition to remove the form-submit handler hacks in d8 is smooth.
By reviewing I ran over the long introductory sentences and learned something new:
> * Summary here; one sentence on one line (should not, but can exceed 80 chars).
Though, I think those could be shorter. So I tried to come up with shorter ones. Also I fixed it to follow the latest casting coding standards. You can see my changes compared to the last patch here.
Updated patch attached.
>I think it's ready to fly.
Agreed, imho this is rtbc.
Comment #34
fagogrml. The git mirror is lacking the latest commits and doesn't include #331951: Figure out and apply coding standard for casting. Re-rolled using CVS.. :/
Comment #35
effulgentsia CreditAttribution: effulgentsia commentedThanks! I agree with all the changes between #31 and #34. Given the complexity of this issue, I think a review from sun, Frando, yched, or chx is in order before it's RTBC. Any takers?
Comment #36
Frando CreditAttribution: Frando commentedWow, great work effulgentsia and yched. I very much like where you took this from #5.
This patch is (once again) an important step towards cleaning up the whole entity/field form mess. Great comments in the patch, btw. That whole stuff really wasn't documented at all so far..
I couldn't find any issues when reading the patch. It really makes node forms quite a big saner. I like that for now, to keep the patch simple and not break BC, we still keep #builder_function while making it optional. And with the node form we show how entity forms can be done properly and simple. Core's other entity forms can likely quite easily be converted in a follow up patches. Afterwards, we can decide if we want to break BC and remove #builder_function or leave that to D8.
Anyway, this patch rocks and simplifies some things we've been trying to fix for quite some while now. While it doesn't do everything we likely want to do here, this patch really achieves most we still can do in D7. And it has plenty of comments and the important @todos for D8. RTBC!
Comment #37
webchickI'm a little turned around by all the various approaches that have been tried here. Could someone please post a summary of what this patch does, and what API changes are required of contributed entity modules to conform?
Comment #38
fagook, let me try ;)
This patch fixes the issue of unvalidated values going into $node, if node_form_submit_build_node() is used together with #limit_validation_errors. For that to fix, we had to fix the node form rebuilding workflow such that it leverages form API persistence, so we don't have to update $node to keep the latest values.
In order to do that this cleans up the node form workflow to work with form API persistence and working towards fixing #367006: [meta] Field attach API integration for entity forms is ungrokable at the same time. So it introduces hook_node_submit() deprecating the hacky custom #submit handler for updating $node values. Still the handler approach works, so no API change here.
API changes:
So once this is in, we still need to fix the same bug for all fieldable/entity forms in core + fix #763376: Not validated form values appear in $form_state to avoid that module developers are able to re-introduce this bug by using node_form_submit_build_node() together with #limit_validation_errors + write docs. A lot left, so let's move on and get this bastard in!
Comment #39
effulgentsia CreditAttribution: effulgentsia commentedThanks, fago. Your explanation is absolutely correct. Since this patch is complex, let me also take a stab at explaining it. Perhaps with both of our explanations together, webchick will be able to make sense of it (and webchick: if you can't, it's our fault for not explaining it well enough).
We're trying to do two things: tighten security and fix some serious WTFs. But both of these are hard, especially when also trying to not break BC, so we're trying to do them in manageable steps.
For both security and sanity, we need to get rid of #builder_function (at least for core entities, but there's an open question about whether once we've gotten rid of it for core entities, whether we still need to leave it around for maintaining BC with contrib entities that are already using it). This patch does that just for nodes (i.e., it makes node_form_submit_build_node() no longer a #builder_function, so if you want to call it, you need to do so explicitly from a submit handler, as is done in node_form_submit() and node_form_build_preview()). This makes it possible for submit handlers of buttons that use #limit_validation_errors (e.g., field_add_more_submit()) to NOT call node_form_submit_build_node(), a key step in preventing non-validated values from being used in a risky way.
Additionally, this patch changes node_form_submit_build_node() to not set $form_state['rebuild'] to TRUE. I don't think that change is strictly necessary in order to achieve the main goals of this issue, but it's a change that makes sense. Setting $form_state['rebuild'] to TRUE belongs in a submit handler. node_form_submit_build_node() is not a submit handler: it's a helper function that may or may not be called from a submit handler. It should have no knowledge of whether the submit handler that calls it wants the form to be rebuilt. For example, after this patch, the two remaining submit handlers (in core) that call node_form_submit_build_node() are node_form_submit() and node_form_build_preview(). The former does not want the form to be rebuilt (unless some bizarre error occurred) and the latter does want the form to be rebuilt. Therefore, it makes no sense for node_form_submit_build_node() to weigh in on that decision.
So, what this requires of module developers is to:
1) not call node_form_submit_build_node() from a submit handler of a button that uses #limit_validation_errors: see the poll.module hunks
2) not use node_form_submit_build_node() as a submit handler (again, it's a helper function, not a full-fledged submit handler): see the book.module hunks
3) not assume that all entity forms have a #builder_function: see the field.form.inc hunks
4) not assume that node_form_submit_build_node() sets $form_state['rebuild'] to TRUE: see the node_form_submit() and node_form_build_preview() hunks
Additionally, module developers are encouraged to, but not required to, use the new hook_node_submit() for affecting how a $node object is built from $form_state['values'], rather than adding a #submit handler for doing that. See the menu.module hunks for an example. node_form_submit_build_node() continues to have some ugly code in it to maintain BC with modules that choose to not follow this recommendation.
Finally, module developers are encouraged to, but not required to, use the new $form_state['node'] instead of $form['#node'] for accessing node information from within a form building function. node_form() retains code that makes this optional.
Nothing yet. This patch is just about nodes, not any other entity type. If we manage to fix the remaining core entity types in a similar way, then we'll need to decide whether to introduce any new requirements for contrib entities or whether to retain BC for them. We're expecting the other core entities to be easier to fix. Nodes carry a lot of historical baggage that we had to deal with in this patch.
Comment #40
sunSince the changes of this patch are helping to prevent pretty obscure security issues, we are better off with breaking BC; though that can happen in a separate follow-up, after fixing other core entities accordingly.
Does anything break in core if #node is removed? If nothing, then let's remove it?
Why do we merge the node from $form_state here? Either needs docs or might be removed.
Powered by Dreditor.
Comment #41
effulgentsia CreditAttribution: effulgentsia commentedRe #40.1: grep HEAD for $form['#node']. Used by book, comment, locale, menu, path, and translation modules. Therefore, likely also used by many contrib modules. It would be serious kitten killing to remove it at this stage of D7.
Re #40.2: @fago or @Frando: thoughts? I'm not too clear on this either.
Comment #42
fagoad #40.1:
Yes, I also have the impression that $form['#node'] is used very often in contrib. While it would be nice to have it cleaned up and removed, there is no cause of removing it that late it in the cycle. Well, it would remove some bloat from the node form, but in my opinion that doesn't satisfy such a drastic change this late in the cycle.
ad #40.2:
Indeed, this line is unnecessarily ugly. It is supposed to update the node's values, such that any node properties maybe not consisting in the form are still in the object afterwards, thus ensuring we still have a fully-built node object. I updated the code, such that it is easier to read and involves less casts + improved the comments. -> My changes
updated patch attached.
Comment #44
fago#42: node_form.patch queued for re-testing.
Comment #46
fago>The client is most likely malfunctioning.
bot, what's up with you? :(
Comment #47
sun#42: node_form.patch queued for re-testing.
Comment #48
fagoThanks sun, I'm glad that the bot at least likes one of us ;)
Comment #49
Dries CreditAttribution: Dries commentedYes, I also have the impression that $form['#node'] is used very often in contrib. While it would be nice to have it cleaned up and removed, there is no cause of removing it that late it in the cycle. Well, it would remove some bloat from the node form, but in my opinion that doesn't satisfy such a drastic change this late in the cycle.
It might be good to sprinkle some "@todo: remove in D8" comments in core. That will remind us, but will also avoid that people use this kind of core code as an example of how to do things in contrib. Just a suggestion, not a requirement.
Comment #50
effulgentsia CreditAttribution: effulgentsia commentedI'm still confused why we're doing this kind of merging rather than
$node = (object) $form_state['values'];
I can think of two possible reasons:- we're intending to support partial node forms as per #367006-91: [meta] Field attach API integration for entity forms is ungrokable (item 6), but node_form_validate() seems to preclude that.
- we don't want to change the object pointer of $form_state['node'] (for example, so that this code also implicitly updates $form['#node'] as well).
If we're doing the second, we should document that. We should also document our position on the first.
81 critical left. Go review some!
Comment #51
effulgentsia CreditAttribution: effulgentsia commentedRe #50: I think this has something to do with it. This patch is shifting the merging from what's being done in HEAD in node_form() to with the patch being done in node_form_submit_build_node(). And since node_object_prepare() does add things to the node that aren't in $form_state['values'], we already do have some notion of a "partial node form", even if not a fully sane one.
79 critical left. Go review some!
Comment #52
fagoad #49:
yep, makes totally sense. The patch does already so for $form['#node'] :)
ad #50,51
->
We don't do that simple cast to ensure $form_state['node'] is always - at any step of the form workflow - a fully-built node object. Thus any value a module might have set on $node e.g. during load or stemming from node_object_prepare() has to be reliable there for that $node now - regardless whether this new value is contained in the form or not. There shouldn't be different built versions of the same object flying around, that just leads to obscure bugs when modules don't expect that. Instead we need to have a single, consistent version of an object - but that means no object built of a simple cast of some form values can replace it.
Indeed, previously it was only ensured that $node in node_form() is a full object containing possible updated values. The node built during submit was something different. E.g. in d6 $node->taxonomy looks completely different than originally at this stage , the structure even depends on the taxonomy form widget that has been used. Then modules working with that - liken token - have the pleasure of trying to cope with all versions. :)
Luckily with the field API and hook_node_submit() things should be much better here in d7. Still, we should have the lesson learned and not throw away the fully-built $node object *we already have* in $form_state, instead update it and so make sure the updated $node is even consistent with a $node returned directly from node_load().
>we're intending to support partial node forms
Indeed, that would be nice and probably easier with that approach. Anyway that's theoretically already possible by setting everything unwanted to #access == FALSE.
>we don't want to change the object pointer of $form_state['node']
I think that doesn't buy us anything apart from saving the $form_state['node'] = $node line ;). I fear $form['#node'] is already a clone at this stage nevertheless as FAPI serializes $form and $form_state separately to cache it, thus we loose object identity here. Anyway $form is rebuilt and so thrown away, thus $form['#node'] gets the fresh object on rebuild. Also in terms of BC, $form['#node'] was not updated in d6 either, so it should be fine that way.
Comment #53
rfaysubscribing. (noting issue summary in #39)
Comment #54
effulgentsia CreditAttribution: effulgentsia commentedBased on above feedback, this patch:
// @todo Legacy support. Remove in Drupal 8.
comment.Comment #56
effulgentsia CreditAttribution: effulgentsia commented#54 failed because I changed node_form_submit_build_node() to not return the node any more, but forgot to update node_form_submit() and node_form_build_preview() to match. I decided to not have it return, because it's easier to understand the pipeline as being about updating $form_state['node']. Having the function also return the node masks that and makes the code less readable, IMO. Note, we're changing the API of node_form_submit_build_node() by taking away the $form_state['rebuild'] from it, so we might as well use the opportunity to make it as grokable in the process as we can. Following that logic, I decided to change its name to node_form_submit_update_node(). Again, the whole concept here is that in response to a form submission, we need to update $form_state['node'] with submitted data (after it's been validated, of course). I think "update" conveys the iterative nature of the process, especially in multi-step scenarios such as the "Preview" button, more clearly than "build". The down-side with "update" is if it conveys the wrong impression of saving something to the database (e.g., the "U" in CRUD, and hook_node_update()), so I'm open to other ideas, but let's pick a verb that we can use in our documentation as well as the function name: i.e., a verb we can use in the PHPDoc Summary Line of hook_node_submit().
Comment #58
effulgentsia CreditAttribution: effulgentsia commentedFixed typo.
Comment #59
effulgentsia CreditAttribution: effulgentsia commentedSince I think we're mostly agreed on where we are so far with this, perhaps it's time to add in the fixes to all core entities. That way, this issue can be reviewed for its full impact. This patch will likely fail. For now, I just want to know where.
Comment #61
effulgentsia CreditAttribution: effulgentsia commentedMore abstraction, but same tests will likely fail.
Comment #63
effulgentsia CreditAttribution: effulgentsia commentedThis one might actually pass. If it does, I'll work on a summary.
Comment #65
effulgentsia CreditAttribution: effulgentsia commentedOne more bug fixed. All the failures from #63 now pass locally.
Comment #66
effulgentsia CreditAttribution: effulgentsia commentedOk, here's the summary of the issue as I understand it: it's long, because the issue is complex and has a lot of history. For those wanting even more background, #367006-91: [meta] Field attach API integration for entity forms is ungrokable contains an excellent explanation of the situation as it was a few months ago. Trying to fix the problem without breaking any APIs seemed hopeless. This patch does break some APIs, but attempts to contain the damage as much as possible.
When the Entity API went in to D7, two things were true:
With the above being the case, the #builder_function property was introduced as a special property just for entity forms with the idea that each entity form could define a custom function for moving data from $form_state['values'] to some other part of $form_state. This function would then have to be called from one of the submit handlers of every button that required a rebuild. So, for example, field_add_more_submit(): the submit handler of the "add another item" button, but not just that button: every AJAX-enabled button you put on an entity form: yuk! But this function wasn't just about implementing multi-step persistence, it also attempted to generalize what we've learned over many Drupal versions as something we need to do for node forms: how to translate from $form_state['values'] to $node, so that after calling it, you can preview the node or save it to the database. The concept is simple: we need to have some kind of thing in $form_state that contains data about the entity across a multi-step workflow, that can be used for previewing the entity at any point in the workflow, and saving the entity once the form workflow is complete: hey, why not make that thing the entity itself!
But then we had to throw in #limit_validation_errors (and when I was working on that, I had no clue about this whole #builder_function business). And in that patch, we added warnings in the PHPDoc of form_set_error() to the effect of "don't do anything risky in a submit handler of a button that uses this". But calling #builder_function is exactly the kind of thing this warning tried to caution against, especially for node forms, but really, for any entity form. #builder_function and #limit_validation_errors are inherently incompatible (as the entity built by #builder_function must always be valid). This is no fault of anyone who worked on the Entity API, as #builder_function came first. It was my fault for not understanding #builder_function when I was working on #limit_validation_errors.
Fortunately, we have a solution. The first bullet point of this issue summary is no longer true: in D7, a form can be rebuilt without *always* moving *everything* from $form_state['values'] to $form_state[SOMETHING] to #default_value, because in D7, FAPI automatically retains user input of the submitted step during a rebuild. This means the buttons that use #limit_validation_errors don't have to (and shouldn't) call #builder_function.
Furthermore, by getting rid of the need to call #builder_function from every submit handler, we get rid of the need for the property at all. Entity forms have a specific known requirement, not an open-ended one. Their requirement is to map information from $form_state['values'] to an entity object (for previewing and saving). They also have the requirement of their form builder callback and hook_form_alter() implementations setting elements' #default_value from information in the entity, and therefore, having an entity from which to do that, whether the entity is one that is loaded from the database (i.e., the first step of the edit form of an already existent entity) or an in-progress one (i.e., within a multi-step workflow, such as after clicking "Preview"). The heavy lifting for both requirements can be centralized to a entity_form_initialize() and a entity_form_submit() function, which is what this patch does.
So, that's basically it. In summary, this patch:
While in some ways this is pretty significant API changing, it's actually not *that* bad. The most commonly used APIs by contrib modules that extend entites remain in tact. $form['#node'] remains, even though deprecated. The possibility to use a node form submit handler to do what is now possible with hook_entity_submit() remains, even though deprecated (the patch converts menu.module to the recommended implementation to serve as an example for how module authors *should* do it). The field attach API hooks remain unchanged. The validation hooks remain unchanged.
The breaks are essentially confined to submit handlers of buttons (so, if a contrib module implements its own "add another item" or "preview" button, it would need to be fixed) and for contrib modules that implement new entity types. For the latter, feedback from Damien and fago would be great, as they've done much work with contrib-defined entity types.
Once this issue is solved, we'll be able to solve #763376: Not validated form values appear in $form_state. Given that we were guilty of not following our own recommendation to not do anything risky in the submit handlers of #limit_validation_errors buttons, we shouldn't rely on it as a recommendation only: we should secure it at the FAPI level.
Comment #67
rfayWow. Thanks for the great summary (and the excellent work on the issue).
Comment #68
fagowow. #66 is an amazing write up and describes the history of this pretty well. However I'm not sure whether its a good idea to provide entity_* form API functions right now - we have too less experience with entity forms in order to be able to tell what practicable ways are to deal with it in order to be really useful for any kind of entity modules might introduce. So the major problem I see with this approach is that it partly takes over the control of the entity form.
E.g.: entity_form_submit() does:
Well, we are used to do it that way in core. However in my case for Rules the rule configurations are entities, but most of the form are dealt with nevertheless by the rules API. Having this code snippet would require me to do much work to clean out the form values in order to get things working - pretty weird.
Also there are use cases where multiple entity forms are needed inside a single $form, e.g. (but not only) for profile2.
So we still need to let the module providing the entity to have total control over the form + the submit process. We don't have enough experience to take over control right now.
We have discussed that all in length in #367006: [meta] Field attach API integration for entity forms is ungrokable and this issue was about getting the *basic* outcome of it into the node form while fixing the actual bug, which was just a symptom of the broken workflow we have now. Your patch though, starts to solve it generically half-way. Writing something like entity_get_form() as proposed in the mentioned issue, that generates a basic entity form + cares for validation and submit surely makes sense. But we not only know too less about the possible entity forms, also doing it properly would involve much more changes - including API changes like fixing node_form_validate() - and is imho way too late and clearly out of scope for D7.
Ok, but what can we do for D7? We need to fix the existing forms, but should leave the control to the providing module. So i really liked the previous approach, of figuring it out for our most complicate case - the node form - first, and then apply the improvements to all entity forms. But well, we could do it all at once too.
Thus, to the actual patch:
What about doing it that way:
In case a module wants to do a custom button + submit handler it could invoke this function to run the usual submit process.
The question remains whether we want to introduce hook_entity_submit() + validate(). As noted above, I don't see any value in those without having hook_entity_form_alter(). So when we do that, we really should add all of them. Basically this would allow a module to alter entity forms in general without making use of the field API. For sure that would be useful.
Maybe we should call all those hooks hook_$entity_type_form_op() instead of hook_$entity_type_op() ? That would makes clear it is about forms and would be more consistent with a possible hook_entity_form_alter().
Makes sense? :)
Comment #69
effulgentsia CreditAttribution: effulgentsia commentedThanks for the feedback, fago. This patch changes back to $form_state[$entity_type]. It re-introduces #builder_function to handle your point about us not knowing enough about the needs of contrib entities. I renamed entity_form_submit() to entity_form_submit_build_entity() and just made that the #builder_function value of comment and taxonomy_term forms and used by node_form_submit_build_node().
I renamed the new hook (what was hook_node_submit() in earlier patches and hook_entity_submit() in #65) to hook_entity_form_submit_build_entity().
I don't think we need to introduce hook_entity_form_alter() here. We already have hook_form_alter(). My main reason for wanting entity_form_initialize() and entity_form_submit_build_entity() is to avoid needless code duplication across the core entities.
Comment #71
fagoWhat's about that point?
So what about my suggested approach above?
* In order to reuse code entity_form_submit_build_entity() could be a helper that is invoked by node_submit() instead of the other way round. This would solve the point I quoted above. Furthermore that way $entity can be passed to the helper, thus the helper doesn't force a specific location of the entity in $form_state. This makes sure the entity providing module can make use of it, regardless how it decides to organize its form.
* entity_form_initialize() does suggest any entity form looks like that, which doesn't play with letting modules control over it? What about that?
* I agree that we don't need hook_entity_type_form_alter(). In case multiple entity forms should be embedded, defining it once as usual + using drupal_retrieve_form() to should do exactly that without the need for another hook. :)
* What about hook_$entity_type_submit() or hook_$entity_type_validate()? Usually we have entity type specific hooks + an optional generic entity hook.
* I dislike #builder_function - that just makes stuff complicated to grasp. Also, as you noted build is not the right word any more as it's just about updating. I'd vote for just using node_submit() for that. However yes, modules might need this function name in order to support wizard like forms generically for all entities. Rather sophisticated, so I don't think we need to support this right now.
Comment #72
effulgentsia CreditAttribution: effulgentsia commentedThis fixes a bug in #69 and has minor cosmetic code and comment improvements.
Re #71: I'm open to us changing node_submit() and comment_submit() from what they are in HEAD to something else, as you suggest. But is that an API change that's acceptable? We have a test that calls node_submit() directly. Are there contrib modules that call either of these functions directly, expecting them to do what they do now? This patch continues with the assumption that the API for these should not be changed. If that's the case, then entity_form_submit_build_entity() (or whatever we want to call it) must call these functions after it merges $form_state['values'] onto the entity, but before it calls field_attach_submit() and hook_entity_form_submit_build_entity() (or whatever we want to call that). That's the reason for 'submit callback': so we can re-use a single function for all the new stuff introduced by this issue, but still call the legacy node_submit() and comment_submit() functions without changing their API and at the correct time in the sequence.
entity_form_initialize() doesn't force entity forms to store their entity at a particular location within $form_state. If you don't want the default behavior, don't call entity_form_initialize(): do what you need to do yourself. And don't call entity_form_submit_build_entity(): use your own #builder_function. Presumably, if you don't have your entity maintained in the default part of $form_state, then it's probably because you're doing something fancy, and need to do your own logic anyway, both in the form builder and in #builder_function.
I agree. But it's what we got in HEAD. If we can get rid of it entirely as I tried to do in #65, then great! But you correctly pointed out the need for flexibility in contrib (and we even have a use-case for the flexibility with node forms). Do we really want to introduce a new name? Maybe, since the concept has evolved. But perhaps, it makes sense to leave it, at least for D7. I'm pretty flexible on this.
This patch changes nothing about validation, and I think we should keep it that way, unless you're envisioning a critical issue needing it. webchick will want a justification for every API change in this patch, so let's keep it as minimal as possible. We already have a hook_node_validate() in HEAD, and are just expanding it here to receive $form_state: needed to not be disingenuous about saying $form_state['node'] is preferred to $form['#node'] (see the hunk in translation.module). We do not have across-the-board hook_ENTITY_TYPE_validate(). If we don't have across-the-board hook_ENTITY_TYPE_validate(), I'm skeptical about introducing an across-the-board hook_ENTITY_TYPE_submit(). That's what prompted me to come up with the hook_entity_form_submit_build_entity() as the name of the new hook: keeps it targeted as to what it's there for. But, I'm open to a better name for it.
Is any of this convincing? If not, want to post a patch with what you have in mind?
Comment #73
effulgentsia CreditAttribution: effulgentsia commentedFrom #68:
Despite what I said in #72, it's actually very easy to convince me to add new hooks, but webchick is increasingly strict about these things (fully appropriate, given her role on the project), which is why I've been resisting the urge, even up to #72. That said, if we can find sufficient justification, I'd be on board. I do think this is one of the areas where it's unfortunate we only went part way with the entity API and left as much as we did to be implemented only on an entity type by type basis. Example: does it really make sense for menu.module's ability to attach menu item control to the node editing form to be tightly coupled to nodes? Any reason for it to not add that same ability for all entities? I'm sure we can come up with lots of use-cases of things that should be able to extend entities that for one reason or another we don't want as a field. Alas, I fear it might be too late to solve this for D7 as completely as we would like to.
Comment #75
sunSo far, I only skimmed the recent follow-ups, but I'd like to know: As long as custom/contrib entity module(s) can alter away the default behavior, I'd highly prefer a commonly expected default behavior for all (core) entities by default. Wouldn't that be possible with eff's patch?
i.e. I'd prefer to have a default way, mostly predefined by core (ideally nuking #builder_function), not only to won't fix support requests that ask for compatibility with module Blargh that will appear in my queues.
Will try hard to read + review more in-depth ASAP.
Comment #76
fagoad #75:
Makes totally sense - agreed.
Of course, in that case it's fine - this wasn't that clear to me. So it needs to be clearly documented as being an optional helper instead of *the* way of doing entity forms.
ad entity_form_submit_build_entity() or using $entity_type_submit():
oh I see now, you did that just to keep BC. Still cannot we instead use a function $entity_type_XY to do the entity-type-module submit logic + call the helper afterwards? Maybe we could pass a flag to the helper to tell it whether it should auto-apply the form values or not. I just don't like having a new entity info key 'submit callback' only for that - there are no form related keys in hook_entity_info() else so it doesn't make much sense to me to have a lonely 'submit callback'.
ad naming:
hook_entity_form_submit_build_entity() is a pretty long and weird name. Not sure about the right namings though, but it should be consistent with the field API stuff already there. So either something with 'submit' or 'extract_form_values' ?
ad #builder_function:
Why do we still need it? We don't need it any more for usual form rebuilding. Thus we would only need in order to make wizard like forms done generically for entity forms possible - something pretty sophisticated we don't need to support at all ;)
ad #72 + hook introduction:
Adding new hooks doesn't break BC, but generally I agree with you. Still having the menu module to implement an entity hook when it altered a node form previously is not really consistent? A node hook would make more sense to me here.
But yes, let's postpone hooks for doing any entity-level form alterations or such to d8 or follow-ups.
Comment #77
effulgentsia CreditAttribution: effulgentsia commentedOk, taking into account above feedback, this patch:
Comment #78
effulgentsia CreditAttribution: effulgentsia commentedFrom #76:
I'd be ok with changing #entity_submit to #entity_extract_form_values if that's preferred.
Comment #80
effulgentsia CreditAttribution: effulgentsia commentedFixed entity_form_submit_build_entity() to return the entity.
I have a thought I'd like feedback on before implementing. What about instead of introducing a new form property #entity_submit, we simply use hook_field_attach_submit()? It's a little odd, because the core uses of this are comment_submit(), node_submit(), and a replacement for menu_node_form_submit(), which are all examples that are specifically working with something about the entity that is not its fields. But in all cases, it is working with a fieldable entity, and I think it's reasonable to assume the same for contrib use-cases, as why would you ever create an entity type that you provide an editing form for and not make it fieldable? It allows us to not introduce a new hook (i.e., hook_entity_submit()) or form property (i.e., #entity_submit). But is it too conceptually weird? Are all the hook_field_attach_*() hooks really intended only for field-related activity? The problem is we do not have the same set of hooks to handle non-field form elements.
Comment #81
effulgentsia CreditAttribution: effulgentsia commentedCross-linking to #629252: field_attach_form() should make available all field translations on submit, as yched did in #21.
Comment #82
effulgentsia CreditAttribution: effulgentsia commentedRe #80: No, using hook_field_attach_submit() for mapping non-field data would be wrong. To properly retain BC, we need #entity_submit, and we need it to run *before* field_attach_submit(). This is also consistent with other entity/field invocation order that we use (for example, hook_node_validate() runs before hook_field_attach_validate()). This patch fixes accordingly.
One may ask, if we're adding a #entity_submit, do we also need a #entity_validate? The answer is no. Because the regular #validate is just fine (at least until we revisit the whole validation workflow in D8). What's special about "submit" and only "submit" is the need to call it from multiple button clicks, where the buttons implement their own #submit, overriding the form-level ones.
So, now, I think this is good to go (well, I'm sure sun or fago will find something wrong with it, but hopefully, nothing major).
Comment #83
effulgentsia CreditAttribution: effulgentsia commentedOne more change: this patch replaces (node|entity)_form_submit_build_(node|entity)() with (node|entity)_form_submit_update_(node|entity)() (see #56). IMO, the verb change conveys the new concept better, and since we're breaking the API of node_form_submit_build_node (by not setting $form_state['rebuild']), it's actually better to change the function name to force contrib modules that are calling it to break in an obvious way rather than a subtle way.
Summary of API changes relative to HEAD:
The above is it as far as breaks. Additionally, contrib modules are encouraged to, but not required to do the following:
Reviewers: as part of your review, please confirm that the above is an accurate and complete list of API changes.
Comment #84
fagoWow, amazing progress. Here is a quick first review, except a more careful one later on:
@naming #entity_submit + (node|entity)_form_submit_update_(node|entity)()
Sounds good.
#629252: field_attach_form() should make available all field translations on submit introduced $form_state['entity']. We should revert to that or fix that to use $form_state[$entity_type] too to avoid doubled up entities in there? Or should we just don't care, as PHP should be able to keep object identity here?
Exactly! Entities don't require fields, so the solution to this shouldn't either. However usually we have entity_type specific hooks for what we have field API attachers + a field API hook. So from that point of view it would make much sense to me to add hook_node_submit() instead of #entity_submit. Also, just like we use this to invoke node_sumit(), we could use the hook for that instead?
While I agree with you that the submit part belongs to the form, there might be multiple, different forms for an entity. So a hook makes sense to me + it is consistent with the other usage of hooks + field API attachers. With #entity_submit though, we partly have it driven by form callbacks + partly by a hook (field API).
Hm, the same way a button could override #validate and thus it would need a way to invoke entity form validation? Though I agree the need for that is not as usual as for #submit.
Comment #85
effulgentsia CreditAttribution: effulgentsia commentedI haven't had a chance yet to review the impact of #629252: field_attach_form() should make available all field translations on submit and how its introduction of $form_state['entity'] affects what we should do with #83.
I reviewed HEAD's entity and field attach APIs, and continue to think that it is better to introduce #entity_submit than hook_node_submit(), hook_entity_submit() and/or hook_ENTITY_TYPE_submit(), at least for D7. For D8, I think we can improve all our entity APIs for greater consistency and less code duplication across entity types, but I'm concerned about introducing any of the aforementioned hooks to D7. Here's why (sorry for the length, but this was my process for trying to grok the Entity API, and I haven't grokked it sufficiently yet to condense it well).
These are our hook_field_attach_*() hooks:
The first set are all invoked from an entity type specific function. So, for example, node_save() calls
field_attach_presave('node', $node)
and comment_save() callsfield_attach_presave('comment', $comment)
. For most of these, hook_ENTITY_TYPE_*() is called. But not for all. For example, taxonomy_term_view() does not calldrupal_alter('taxonomy_term_view', $build)
the way that comment_view() callsdrupal_alter('comment_view', $build)
. Was this an oversight? A performance optimization? For purposes of this discussion, it doesn't matter: what matters is that as long as we're delegating the decision to the entity type, we have to assume that these inconsistencies will exist. This is what I hope we can improve for D8, but I don't expect us to replace ENTITY_TYPE_save() and ENTITY_TYPE_view() functions with generic entity_save() and entity_view() functions in D7.For 'purge', that's a field-specific concept, so there's no entity-level equivalent hook.
The 'load' hook is interesting. This is one that does go through a generic entity_load() function, though with the ability for a given entity type to override implementation using a custom controller class. If the default class's attachLoad() method isn't overridden and the 'load hook' within hook_entity_info() isn't overridden, then the generic implementation does invoke both hook_entity_load() and hook_ENTITY_TYPE_load(). This is the only case of an ENTITY_TYPE hook being invoked from a generic entity function rather than an entity-type-specific function, though as mentioned, either the controller class or the 'load hook' can be overridden to customize that.
All of the above have nothing to do with forms. They act on entities and fields the same way regardless of what form was used to edit the entity. A module that wants to implement an alternate editing form for a given entity type doesn't need to worry about any of those hooks.
field_attach_form() and field_attach_form_validate() (which calls field_attach_validate()) are called from forms. So, not just entity-type specific, but form-specific. So, comment_form() calls
field_attach_form('comment', $comment, $form, $form_state);
, but if a module wanted to implement ALTERNATE_COMMENT_form(), it would need to also callfield_attach_form('comment', $comment, $form, $form_state);
. In other words, for the 'form' operation, the Field Attach API is being used to attach a set of fields to a form, not to an entity. There is no corresponding hook_entity_form() or hook_ENTITY_TYPE_form() because we have hook_form_alter() and hook_form_FORM_ID_alter() instead (though I can see some benefit to adding hook_entity_form() and hook_ENTITY_TYPE_form() to D8). Similarly, field_attach_form_validate() is called from a FAPI #validate function. An alternate form for the same entity type may want an alternate #validate function, and that alternate #validate function would need to call field_attach_form_validate() where appropriate. We do not have hook_entity_validate() or hook_ENTITY_TYPE_validate(), because hook_form_alter() can customize #validate, so for whatever reason, we chose to let D7 code rely on standard FAPI rather than introducing Entity API hooks for this. As you said, it's rare for a button to specify a custom #validate, but if it does, then it's probably because it wants custom validation, and in this case, it would need to handle how it wants to integrate all of the form validation, including the field widgets. Note, we do have hook_node_validate(), but I think only for legacy reasons. We do not have hook_comment_validate() or hook_taxonomy_term_validate().That brings us to the last one, 'submit', and the one this issue needs to address. The problem here is that we have something that FAPI alone can't handle properly (different buttons with different submit handlers, several (but not all) of which require $form_state['values'] to be mapped to an entity object). Ideally, we should solve this with a generic helper function, entity_form_submit_update_entity(), as many entity types will have this requirement. Advanced cases can implement an alternate function, but the generic one should be able to do the job most of the time, so we need the generic function to call either a well-named hook or a set of functions in a $form property. If we only needed this feature for nodes, then hook_node_submit() would make total sense, as we already have hook_node_validate(), but we need this feature for all entity types. Having a generic entity function call hook_ENTITY_TYPE_submit() would be introducing something only paralleled by entity_load(), but entity_load() does it via a controller class, and I don't think we want to introduce that level of complexity to solving this issue. If we do want to do it as a hook, then at the least we should introduce a 'submit hook' key to hook_entity_info() to fully parallel how 'load' is handled. On the other hand, introducing a $form property like #entity_submit seems fully consistent with how we're handling validation via #validate.
Thoughts? As I said, I'm still learning the Entity API, and am open to alternate ideas if I can fit them into some kind of conceptual understanding that makes sense.
Comment #86
effulgentsia CreditAttribution: effulgentsia commentedHere's an approach I really like, and hope fago and sun do too. This adds the concept of #pre_submit to FAPI. It makes sense really: the concept of handlers needing to translate $form_state['values'] to something persistent in $form_state, and decoupling that from what a specific submit handler then does with the updated $form_state extends beyond entity forms, so I think this is a logical and valuable addition to FAPI. This then adds a entity_form() function instead of what in prior patches was entity_form_initialize(): using a similar pattern established by user_account_form() and system_settings_form() (the pattern of passing $form and $form_state to a helper function so that common needs across a class of forms can be centralized). It re-adds hook_node_submit() for nodes only. This creates consistency with hook_node_validate() existing for nodes only. Other entity types may choose whether or not to add hook_ENTITY_TYPE_(validate|submit)(), but ideally they should either add neither or both. These are a convenience only. A module can use #validate and #pre_submit to achieve the same thing for entity types that do not invoke the corresponding hooks. Finally, it changes what in earlier patches was $form_state[ENTITY_TYPE] to $form_state['entity'], since that's what #629252: field_attach_form() should make available all field translations on submit established. It provides $form['#' . ENTITY_TYPE] as an alias for it, since that helps code readability within entity-type-specific form processing functions and is what people are already used to with $form['#node']. It ensures object identity between $form['#TYPE'] and $form_state['entity'] after form cache retrieval by adding a #process function.
Comment #88
effulgentsia CreditAttribution: effulgentsia commentedHm. This isn't so good at handling translatable fields: yay for committing #629252: field_attach_form() should make available all field translations on submit which has the necessary test for this! I added a workaround to make the test pass (no worse a workaround than what is in HEAD), but an @todo to the patch, because it really does expose a flaw that would be good to solve correctly. Any ideas on how to do that?
80 critical left. Go review some!
Comment #89
effulgentsia CreditAttribution: effulgentsia commentedThis patch includes an entity_form_pre_submit() implementation that solves #88 nicely, I think. And in the process reverts field_default_submit() to the nice simple function it was prior to #629252: field_attach_form() should make available all field translations on submit.
Comment #90
yched CreditAttribution: yched commentedre #88 : well, this makes makes the actual extraction of form values into entity field values be performed by both entity_form_pre_submit() and field_default_extract_form_values() (invoked in field_attach_form_validate() and field_attach_submit()).
Also, doesn't this make this comment a bit off ?
// Allow fields to customize how their form values map to the entity.
Is it workable to have entity_form_pre_submit() do something like :
and leave the merging of field values to a field_default_X() op through f_a_submit() ?
minor : the common var name for an array of instances is $instances rather than $field_instances - but well, that's usually within Field API code, where the context is clear, so YMMV.
Comment #91
effulgentsia CreditAttribution: effulgentsia commentedImplements #90. Thanks!
Comment #92
fago@#pre_submit
I initially liked the idea very much, but thinking longer about it I'm unsure.
I've already thought about introducing a new step too, but it's not solving the real problem. We are facing the problem of buttons being able to override #submit, but they still might want some default behavior to occur - which is in our case, extracting form values. Having a separate #pre_submit that is not overridden solves it in our case, but still if the button overrides #pre_submit we might run into the same dilemma on #pre_submit level. So we are not really fixing the problem, we are moving it a step away from us ;)
The existing d6 code, which hacks around with the form state handlers, does that thing of invoking default behavior good. But it could need a clean API function like form_execute_default_handler('submit', $form, $form_values) to do it. So what about following the existing approach of using $form['#submit'] handlers for form values extraction? Then this default submit handlers extract the form values, but only if explicitly invoked. The 'Save' submit handler can so easily leverage that.
That way also buttons don't invoke this default behavior by default, which I think is superior than requiring buttons using #limit_validation_errors also to override #pre_submit to an empty array in order to actually fix this issue. Thus instead of introducing a "pre" step, we would introduce the opportunity to invoke the default behavior if required.
Note that form_execute_default_handler() would also fix the problem for validation too, as a button introducing a new validation handler, but which wants to update $node/$entity, still needs to fire the usual validation. Again it could just invoke the default behavior.
Yes, another solution would be to provide according functions node_validate(), node_submit() for invoking default behavior that make use of appropriate hooks - but a) it wouldn't apply to any added handlers what is still possible b) using the logical function names node_validate(), node_submit() is not possible right now for that as it would change existing functions and so break BC, c) it would not solve the problem generically for other forms too, d) we would be back to requiring lots of new hooks.
So it looks to me that form_execute_default_handler() seems the way to go. Opinions on that?
If we can agree on that, I could take it on.
ad $form['#'. $entity_type ] for holding the $entity:
I really think we should NOT do that. Imho $form['#node'] is there due to a previously, for something like that unusable $form_state. But now, we have fixed that and can use $form_state, so we really should do so. The currently edited entity belongs to the state, because it might have been updated during the workflow - so it's no property of the form itself. In contrast to that $form['#entity_type'] can be seen as form property - it never changes across the whole form workflow.
Also the process handler of fixing object identity doesn't solve having doubled entities living in the form cache, thus bloating the form "cache" for nothing.
>It re-adds hook_node_submit() for nodes only. This creates consistency with hook_node_validate() existing for nodes only. Other entity types may choose whether or not to add.
Sounds good!
+ // @todo $form_state is not passed to node_validate() for legacy reasons only.
+ // Should this be changed for Drupal 7 or wait until Drupal 8?
I'd say so. It's a) necessary for code to be able to properly make use of $form_state['node'], b) it just doesn't make sense to pass $form, but no $form_state, c) it doesn't introduce BC troubles.
ad entity_form_validate() and attach field handler:
entity_form() doesn't hardcode the location of the fields in $form, which is great. However entity_form_validate() and also the pre_submit handler do so. That doesn't play so well together. Perhaps we should just provide a entity_form_fields_validate(), which needs to be called on the right $form and let modules invoke the submit attacher theirself?
Having entity_form_pre_submit() just to care about doing the default form values extraction looks good to me.
+ entity_form($form, $form_state, 'comment', $comment);
+ $comment = $form['#comment'];
Shouldn't we bake the second line somehow into entity_form() as it's needed nevertheless? While I like entity_form(), the naming suggests it returns a ready to use form, what is not the case - therefore I liked the old name more.
Comment #93
effulgentsia CreditAttribution: effulgentsia commentedI fear reaching consensus on some of these points is proving tricky.
Here's a way scaled back patch. All it does is:
I think a lot of the other ideas that have been explored here are great seeds for D8, and maybe some of them can even be follow-up issues for D7. For example:
But lets try to bring this issue to a close by targeting it to what is absolutely necessary.
Comment #95
effulgentsia CreditAttribution: effulgentsia commentedChanged the comment, node, and taxonomy builder functions to call f_a_submit() before other submit functions/hooks. This is necessary because HEAD does a full $form_state['values'] to entity dump before starting the rest of the submit_build logic. But with this patch, we don't do that, as per #90. So, calling f_a_submit() sooner is more similar to how HEAD works. Fields are supposed to be encapsulated (not be tightly coupled to non-field entity data), so there should be no unwanted side effect from calling f_a_submit() sooner.
81 critical left. Go review some!
Comment #96
fagook, I agree that taking smaller steps is the way to go.
I think it was only there because the field API submit callback required it that way. I don't think removing it is a big deal as entity form details change anyway, so just do it. We have already quite some places were code is unsetting it afterwards again, which is just weird. Also it really has nothing to do with building the entity.
I do think we should do form_execute_default_handler() in D7 in order to make entity forms properly extensible. But maybe also a hook is the way to go. Either way, let's keep it out of this issue and do it in a follow-up (or even in d8) - let's concentrate on fixing the basic workflow here. However as it's not clear yet what the way to proceed is, the comment in node_form_submit_build_node() shouldn't say so, so I improved the comment to keep the solution to the problem open.
Apart from that I:
As I used git to roll the patch, you can inspect the changes I made to #95 here.
Comment #98
fagoupdated patch to fix vocab form and fixed the user form to not overwrite existing passwords.
Comment #99
effulgentsia CreditAttribution: effulgentsia commentedThanks, fago, for your reviews and patience while I stumbled through properly understanding the problem-space. I agree with the changes described in #96 and a cursory look at the git history makes sense. I'll try to do a full review of the entire patch tomorrow, just to try to catch any remaining details, but I think you and I reached agreement on everything significant, so all that remains is just re-checking the details.
Comment #100
effulgentsia CreditAttribution: effulgentsia commentedUpon closer examination, #98 continues to look great to me. Minor nits:
Do we need to set 'rebuild' to TRUE in the corresponding "else" block?
I agree with you that we shouldn't discount the possibility that a button-level submit handler triggering form-level submit functions is potentially a useful pattern we may want to formalize and take advantage of. But for this specific use-case, we're encouraging people to use hook_node_submit(). So, how about something like this comment:
In this case, should we remove the code that sets it to FALSE at the end of taxonomy_form_term_submit()?
So at the end of this process, we're setting $edit to an array containing all properties of $account, even those that weren't edited by the form. We're making a special exception for 'pass' only. Is this ok? Can we have a comment explaining this? It seems weird to me for the variable to be named $edit, but for it to contain things that weren't edited, but given that HEAD is a bit odd anyway in how it handles the entity / form values relationship, this is probably the right code.
79 critical left. Go review some!
Comment #101
fagoad #99: Thanks for your great help solving this properly. :) I'm glad that we can pass on to the details now!
ad patch:
* I've added those rebuild + taken over your comment.
@modules/taxonomy/taxonomy.admin.inc
I left it there together with its comment as it makes clear the desired behavior of the form. Well, it's not needed, but making it explicit is imho good in this case, as it's not the usual "redirect upon submit".
@user-save:
Indeed, I was a bit unsure about this too. I thought about a fix like the following, so only really edited values are taken over:
However, with user_save() converting $edit back to a pseudo $entity for calling the field API attacher, I fear that this pseudo object might not contain all field values. E.g. consider a field doesn't show a form in a special case, then it's value wouldn't be in $edit and thus an empty value would be inserted for it?
Therefore I think it's better to put all properties in $edit. I added a comment explaining that.
Attached an updated and rerolled patch. Again you can inspect the git changes I made here.
Comment #102
effulgentsia CreditAttribution: effulgentsia commentedI'm not so sure. field_attach_update() clearly says:
// Leave the field untouched if $entity comes with no $field_name property,
. But I also don't see harm in doing it the way the patch does it. Given that we're fixing the call to field_attach_submit() to use a real entity rather than pseudo entity, I don't see a clear answer for which way is more compatible with HEAD. So I think we just need to pick one, and I'm okay with #101's approach.How about merging into one comment:
78 critical left. Go review some!
Comment #103
effulgentsia CreditAttribution: effulgentsia commented@sun, @Frando: this would be an excellent time for one or both of you to review the patch as well. Please include your thoughts on #101/#102 as part of your review. Thanks! I think we're very close to this being ready!
Comment #104
fago@user-saving:
In case we do not need to pass all data in $edit, I think we should not do so. I really dislike our user saving API, but as $edit is supposed to hold only really changing properties we should follow that. Thus I've added in the respective hunk as I posted in #101 and re-rolled the patch.
@patch: Indeed, I think the patch is good to go now. So as said, it would be a good time for any additional reviews.
Comment #105
effulgentsia CreditAttribution: effulgentsia commented#104 looks commit-worthy to me. But it really should have at least one more reviewer before being RTBC. Earlier summaries are on #66 and #83. Here's an updated summary of what #104 does.
Removes calling #builder_function from buttons that use #limit_validation_errors or otherwise are "local" buttons that can simply update $form_state without invoking the full #builder_function processing:
- book.module
- field.form.inc
- form_test.module
- poll.module
Fixes entity form workflows such that:
- comment.module
- field_test.entity.inc
- node.pages.inc
- taxonomy.admin.inc
- user.pages.inc
Rolls back some of #629252: field_attach_form() should make available all field translations on submit, as the fixed workflow no longer requires those hunks to achieve what that issue needed. Note that the tests from that issue remain and pass.
- field.attach.inc
- field.default.inc
Adds helper functions
entity_form_field_validate()
andentity_extract_non_field_form_values()
to assist entity forms in doing the fixed workflow correctly. This allows us to comment the stuff that is tricky in one place, helping with grokability as per #367006: [meta] Field attach API integration for entity forms is ungrokable. Entity forms may choose whether to use these helper functions or do something different, so advanced contrib use-cases that we may not be thinking of yet aren't stuck with any pre-set behavior, as per #68.- common.inc
Adds hook_node_submit(), because HEAD already has a hook_node_validate(), and having one without the other makes no sense. Also, as per #4, the pattern of a button-level submit handler calling form-level submit handlers doesn't appear anywhere else in core, so this hook gives modules a way to transition away from that pattern and into a hook system, which is more consistent with the rest of the node system. BC is retained for modules that choose not to transition.
- menu.module
- node.api.php
Comment #106
sunAwesome analysis and collaboration here, guys! Has been a pleasure to read.
This patch looks mostly RTBC to me. Only relatively minor things remain (compared to the complex and critical heart of this patch).
After reading the docblock of this function, the actual function name feels irritating. The entity is not extracted. Instead, form values are selectively extracted into entity properties. At the very least, a function name like the following would be more appropriate:
entity_form_extract_non_field_values()
However, I'm quite sure we can find a better name than that... thinking out loudly...
entity_update_from_form()
entity_form_properties()
entity_form_values_to_properties()
I feel that a proper function name is important here, as this function plays a key role in the process, and anyone who reads this function name will have to read, parse, associate, and understand its meaning, in order to understand what's going on (elsewhere) -- hopefully without having to look up the actual function body to understand what it does.
Ideally, short and concise; but properly expressing the purpose and meaning of this function is a challenge.
Bonus points for opening a new issue that removes the custom styles in book.css in favor of our new, generic CSS classes in D7.
It looks like these default properties only need to be set for the initial form build; whereas a comment explaining initial form build stuff is only following after these lines. Can anyone think of a condition we could wrap this code into to clarify that? ++consistency
(and elsewhere) Shouldn't we at least test for !empty($form['#builder_function']) before trying to invoke it? In case fago goes nuts during the D7 cycle and reinvents everything, he most probably needs a way to set #builder_function to FALSE, NULL, or empty string...?
If we are able to remove this, we should do so and break BC here. We already know from the past that one thing should always be one type of thing. Mixed data types may save us 10 minutes now, but will bury down countless of hours/days/weeks of developer time later on.
We definitely want to remove this for D7 already, since this entire entity form structure and handling is complex enough, so we absolutely need to decrease confusion where possible. Duplicating this data into $form makes no sense at all and is plain confusion.
However, as both of you mentioned before, let's do that in a separate issue.
oh hey, Taxonomy's vocab form constructor is sound! This clear condition and code flow pattern, we can also use in comment_form() at least, as mentioned above. Possibly elsewhere, too?
I'm pretty sure that at least fago would also like user_register_form*() functions to be adjusted accordingly. :)
Is it possible that this is targeting {users}.data hickups, but perhaps not aware of latest changes via #721436: Remove magical fairy saving of cruft from user_save() ?
After addressing these, this patch would look ready to fly for me, so please proceed.
Powered by Dreditor.
Comment #107
effulgentsia CreditAttribution: effulgentsia commentedThis patch renames entity_extract_non_field_form_values() to entity_form_submit_build_entity() and moves the call to field_attach_submit() into it. I think this makes sense given that we're providing the entity_form_field_validate() function: the more parallel our organization structure between validate and submit, the better.
This patch also cleans up and makes consistent the way the *_form() functions get/set the entity in $form_state.
I'm open to discussion on this, but I don't think so. I think a form either has a #builder_function (which it should if it has more than one submit button like "Save" and "Preview") or it doesn't (like the user profile form, which doesn't have "preview" and inlines the entity building into user_profile_form_submit()). I think it would help consistency to always use a #builder_function (even for the user form, where there's no "preview" button), but that's a code organization decision that's ultimately up to each entity form. In any case, any particular submit handler needs to call SOMETHING to get the entity: whether it's a literal function, the way user_profile_form_submit() does it, or whether it's a variable function like $form['#builder_function']. But I don't think it makes sense to not have anything to call.
Regarding breaking BC in comment_submit() to no longer support an array. My preference would be to take that up in a separate issue, perhaps the same one that argues for breaking BC of $form['#node']. I fear webchick will be stressed out enough about this patch as it is, I don't want anything unnecessary getting in the way.
@fago: want to roll that in to this patch, or leave it for a follow-up issue?
I'm not sure. Perhaps fago can speak to that. But my inclination is to agree with #104. It doesn't really make sense that user_save() takes an $edit parameter. node_save() and other entity saving functions don't do that. But it does, and therefore, we should call the function the way the API dictates: only with values edited by the form.
Comment #108
effulgentsia CreditAttribution: effulgentsia commentedFYI: unrelated to forms, but for more fun with entity complexity and trying to come up with a semi-sane solution without breaking BC: #823428: Impossible to alter node URLs efficiently.
Comment #109
chx CreditAttribution: chx commentedFirst, while I know we keep personal issues out of the queue, allow me a rare comment of extreme happiness that this patch got this far without involving me at all. That effulgentsia is awesome we have seen in the #limit_validation_errors but this goes far beyond. Lots of thanks to fago for help, too.
Issue summary is in #66 and in #93 .
The API breakage is sort of minimal but it really needs good documentation because it's also very subtle: the node form now has it's
$form_state['input'] = array()
removed. Which means that if you have button level submit handler on, say, preview which does something to$node->foo
then that change get's overridden by the user input when the form is rebuilt. I think at least this is what happens. And that's fine but it needs to be documented.I also think the patch is ready but it's really fago who should RTBC this.
Comment #110
fagoad #109: I totally agree, effulgentsia does an amazing job in a lots of issues! ++
@entity_form_submit_build_entity()
Sounds good to me, the naming fits to '#builder_function'.
> Is it possible that this is targeting {users}.data hickups...
No, it's just about using the API as it is. We should pass in there what is being edited.
hehe, I don't think I'm gonna re-invent the existing entity forms in contrib ;) The point is that we are not stuck with doing it that way, so a contrib can use $form['#builder_function'], but it can also follow another pattern. Maybe we can come up with a superior pattern dealing with all cases for D8 though. Anyway I agree with effulgentsia that it's not necessary to check it, if the node_form creates it, it may rely on it afterwards.
As mentioned the user form doesn't have the builder function, furthermore it's not even possible to re-use user_profile_form_submit() for building the entity only, as it has the call to user_save embedded. However I think this is fine for now, as this patch does not deal with making all entity forms properly extensible or such, it just streamlines the workflow of the entity forms as they are now. The node form is already properly extensible, the user form is not - this patch doesn't change that. We have agreed upon caring for that in a follow-up so we can bring this one home first, so we can concentrate on the remaining fixes/improvements afterwards.
@$form['#node']
>We definitely want to remove this for D7 already, since this entire entity form structure and handling is complex enough, so we absolutely need to decrease confusion where possible. Duplicating this data into $form makes no sense at all and is plain confusion.
Make sense to me, but I agree with effulgentsia that we should avoid breaking BC here as far as possible, so that those improvements don't hold up this important patch, which is already late. Let's deal with them in a separate issue.
@form-API persistence: Indeed, still if one needs to change the form elements/values one can disable partially the FAPI persistence as this patch does for the poll.module. I agree that this needs to be documented, but note that this is now default behaviour for any drupal form. So it should be documented globally, if not done yet?
>I'm pretty sure that at least fago would also like user_register_form*() functions to be adjusted accordingly.
Thanks to the form API persistence mode altering the user register form is fine now. There is even a test case for that in core, so I'm happy with that. If a module wants to add more complex rebuild operations on that form, it can do so. But I agree that user_register_form, user_account_form and such should use $form_state to put $account into and for tracking the state (is it register or not?), but I think it's out of scope for this issue. I'm even unsure whether this should be fixed this late for D7 at all (why?), but let's discuss that in another issue.
To me the patch is ready. So if sun is satisfied with the changes/answers, we can finally RTBC it.
Comment #111
effulgentsia CreditAttribution: effulgentsia commentedWow: thanks for all the kind words :) I enjoy working on issues like this one: I learned a ton from fago and Frando in the process.
#109:
#110:
rfay and jhodgdon are working on an issue to add high-level $form and $form_state documentation prominently at the top of form.inc: #802746: Document $form_state and form builder function in form_api group. Here's what's there currently about $form_state['input']:
If that's the correct issue to address the documentation requested above, but you have feedback on it, please comment on that issue. If you think a new documentation issue is needed, please open one. Thanks!
Comment #112
moshe weitzman CreditAttribution: moshe weitzman commentedLets not let this sit too long. chx effulgentsia and fago have approved.
Comment #113
Frando CreditAttribution: Frando commentedWhile I unfortunately do not have the time to do a proper, line-by-line review at the moment, I approve the direction of the patch as well. I properly reviewed the patch in #36, and while it has changed quite a bit since then, I very much like the direction it went. As has been stated several times, a completely clean and generalized solutioln won't be feasable for D7, but this patch goes much further than I thought would be possible. Amazing.
While I still do not like #builder_function a lot, I agree that keeping it might be the best for contrib and for allowing button-level submit handlers to update the entity.
I also like that we added entity_form_* helper functions. This makes it much easier for standard contrib entity implementations.
Anyway: This patch is VERY important and brings much much needed sanity to entity form handling. It has improved incredibly from my first patch in #5, awesome work fago and effulgentsia.
Let's get this in!
Comment #114
Dries CreditAttribution: Dries commentedWoop, woop. Big patch, buy in from many different people, and a nice improvement. Committed to CVS HEAD. Thanks.
Comment #115
rfayThere are issue summaries in #66 and #93, but I'd appreciate it if one of you could update the issue summary here, to summarize the final impact of this patch. It took many forms in its long life.
Also, could you please summarize the impact of the API change and what it means to existing code?
Are there any documentation impacts?
Thanks,
-Randy
Edit: chx in #109 says
chx in IRC also says
Comment #116
effulgentsia CreditAttribution: effulgentsia commentedThe first half on #105 lists the API changes. In summary:
What chx and fago are referring to in #109 and #110 is that we never documented the impact of #622922: User input keeping during multistep forms in http://drupal.org/node/224333 and probably should. D6 multi-step forms were hard partly because a submit handler always had to transfer ALL submitted form values to somewhere in $form_state['storage'] (or within a $form property like $form['#node'], but that was even more confusing), and the form builder function and all form alter functions had to set ALL #default_value properties based on what was in $form_state['storage'], or else what the user had just entered would be lost in the rebuilt form. In D7, it's no longer necessary to go through this complex process for elements that you're simply going to re-display with whatever the user had just submitted, because $form_state['input'] is retained during the rebuild (this is why "add another item" buttons no longer need to call #builder_function). But that means in situations where you do want to clear some of the user input in the rebuilt form, you have to do so explicitly. See poll_more_choices_submit() for an example.
Finally, this patch added a hook_node_submit().
Comment #117
fagoAwesome to see this finally land!
#116 is a good summary. I'd just better clarify what chx pointed out before in #109: We have finally left the FAPI persistence as implemented in #622922: User input keeping during multistep forms enabled for the node form too. Previously the node form disabled this feature, now the form makes use of it.
I've created a follow-up issue for the part we have not handled with this patch yet, but might want to solve for D7:
#830704: Entity forms cannot be properly extended
Comment #118
sunGood job. :)
The only concern that just crossed my mind: In some of the comments above, we identified that the (old) logic/flow could lead to security issues or otherwise invalid data. Would it be possible to add a functional test to ensure that we don't break this relatively complex construct and underlying considerations in the future?
Comment #119
effulgentsia CreditAttribution: effulgentsia commentedI believe #763376: Not validated form values appear in $form_state will take care of that.
Comment #120
fagoYep, I agree that we need to fix the underlying FAPI problem so that the form values are always safe again. Then it's impossible to trigger this issue again as long as $_POST is not directly accessed.
Comment #122
effulgentsia CreditAttribution: effulgentsia commentedAn interesting side-effect caused by the better entity form workflow: #844388: Taxonomy terms disappear from node preview if previewed more than once.