Closed (fixed)
Project:
Drupal core
Version:
7.x-dev
Component:
forms system
Priority:
Critical
Category:
Bug report
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
30 Dec 2009 at 23:05 UTC
Updated:
28 Jun 2019 at 16:19 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #1
cdale commentedI don't know if it is related, but I've seen the error @yched describes in my own AHAH coding, please ignore if way off track, as this comes from my Drupal 6 experiences. I am not as familiar with Drupal 7.
The cause is that $form['#action'] gets cached wrong. This issue will only manifest itself when an AHAH callback is made, and then on form submit, a validation error occurs resulting in the form being re-rendered, without a rebuild.
The reason:
Form is built form first time and cached, #action is node/add, as expected.
AHAH callback is made, and form is rebuilt and re-cached. Action is now path of AHAH callback, in this case system/ajax. (NOTE: At this point, the form in cache is now out of sync with the HTML on the users end, as the user still has node/add as the form action, yet the cached form has system/ajax.
In the event a validation error now occurs on form submit, the form is re-rendered, (not rebuilt), and because it is re-rendered from the cache, the form action is now system/ajax.
Again, I'm not 100% sure if this is relevant to the issue described, but it might help in fixing it.
Comment #2
sunWild assumption, not tested, so please test. ;)
Comment #4
sunSimpler replication:
1. Go to node add form
2. Click "Add another item" button
3. Click "Save" button
4. Optional: Enter title, hit "Save" button again. #action is 'system/ajax' here already.
Since we rebuild the form, the form is rebuilt. And since the form is rebuilt, system_element_info() assigns a new #action, defaulting to request_uri().
This patch fixes it for me.
Contains some other cleanups, which could technically be removed from this patch, but I left them in, because I tried to fix this at the very wrong location at the beginning...
Comment #5
sunComment #7
yched commentedPatch #5 does fix the issue of "taken to system/ajax URL with JSON output after step 4", but the node that gets saved is empty.
No title, no field value, no nothing :-).
Comment #8
sunwell, so why don't we fix that? :P
Comment #10
carlos8f commentedI'll check this out a little later.
Comment #11
yched commented#8 works for me with manual tests.
But it seems the bot won't install D7 with it. Let's see if a re-test makes us lucky :-/
Comment #14
yched commentedI didn't know that test slaves do their drupal install by submitting forms, but the patch does break installer forms.
When you reach the 'Database configuration' form, hitting submit takes you back to the 'select install language' page.
Looking into the HTML, the
<form>"action" attribute is-
install.php?profile=standard&locale=enwith patch-
install.php?profile=standard&locale=enin an unpatched HEAD.drupal_attributes() runs check_plain() :-). Should be OK for the other bits ('accept-charset', 'method', 'id'), but not for 'action'.
Let's try this one.
Note that the hunk dealing with build ids disappeared in patch #8, but I kept sun's patch name for now.
2 days to code freeze. Better review yourself.
Comment #15
carlos8f commentedhmm. Hold on, aren't literal ampersands supposed to be escaped with entities in XHTML attributes? If so, the patch in #8 is more correct than HEAD.
http://www.w3.org/TR/xhtml1/#C_12
Comment #16
yched commented@carlos8f: possibly, but if so that's a different fix in a different issue :-)
Comment #17
carlos8f commentedOh, I assume you meant
check_plain()was getting run twice on the URL. In that case, ok :PComment #18
sunThis is RTBC, unless the bot proves us wrong. ;)
Comment #19
yched commented@carlos8f: er, that's not what I meant back then, but that's actually what was happening ;-). check_url() does run check_plain(), so patch #8 ran check_plain() twice, and current HEAD and patch #14 do encode their HTML entities (once) in 'action' attributes.
Anyway. This is good when the bot says so.
Comment #20
carlos8f commentedYeah ignore what I said in #15, the install bug was caused by the URL being double-encoded by both check_url() and check_plain().
Comment #21
carlos8f commenteddidn't mean the status change :-/
Comment #22
webchickWe need some tests for this, IMO. Sounds like a tweaky bug we are unlikely to catch again if we break it.
Comment #23
yched commented@webchick: The simpletest framework cannot test this :-(.
The bug involves #ajax form submission, and there's no working with a form past a $this->drupalPostAJAX(). This would require the internal browser to parse the JSON and manipulate the DOM the same way the ajax.js script does.
Back to RTBC.
Comment #24
rfayWorking on #484838: [rfay version] AJAX form can submit inappropriately to system/ajax after failed validation as well. I came to a completely different solution there. It seems wrong to me that system_element_info() sets the default to request_uri() in the first place.
Comment #25
chx commented#484838: [rfay version] AJAX form can submit inappropriately to system/ajax after failed validation had a better title.
Comment #26
Scott Reynolds commentedI have as well tested this bad boy and it solves the issue. This issue also will need to be backported as it does in fact happen on D6.
In regards to a test, I do not believe you need to parse json and change the dom. I think you just need to hit the 'add more button' then submit the form without the title. Then 'rebuild' the form based on the form build id and the form cache.
Its not as simple as Im making it sound, but it is doable. We are not checking to make sure the add more button works but checking to make sure the next time we try to build this existing form that the form action is right.
edit: ok spent time on trying to write one. Still think its possible. just need two urls that pull up the same form_id. The form function sets $form_state['cache'] = TRUE and the second url takes the $form-build-id from as a parameter to rebuild the existing form. Then it will render the form which should have the same action (use xpath to find it).
Comment #27
rfayI tested the patch in #14 against the test module posted in #484838: [rfay version] AJAX form can submit inappropriately to system/ajax after failed validation and confirm that it solves the problem described there.
I also tested against the original complaint (poll module malfunction) in #484838: [rfay version] AJAX form can submit inappropriately to system/ajax after failed validation and it resolves that problem as well.
Comment #28
Scott Reynolds commentedOk got a test to work.
So while this bug presents itself on AJAX forms, it really is a bug with the form cache. And by that I mean, when a form is put into the cache and another callback pulls that exact form_build_id out of the cache at a different url, the form action is overridden to the current url.
So my approach to the test was the following, create a bare bones form and set the form_state[cache] = true. Use xpath to find the form_build_id and pass that build id to another menu callback that will pull that form out of the cache and render it to the screen. Use xpath to ensure that the form just render to the screen from a different url has the same action.
This is mimics the essential flow of what ajax_get_form() does, but fetching the form_build_id from a url parameter instead of $_POST.
The old behavior doesn't cause a problem as AJAX would never render the entire form again, just the parts it needs so the $form['#action'] was effectively ignored. But when you try to really submit the form and it fails validation, the form is rebuilt from the cache (which was set by the AJAX callback at system/ajax) and now it renders the whole form with the bad '#action'.
And when you use form_state['storage'] the url is always the same, so the 'bug' went unnoticed there.
Hope that makes sense. This patch is from #14 with a new test.
Comment #29
Scott Reynolds commentedText update. Clearly I was tired last night.
should have been
Doh!
Comment #30
rfay#29 failed the bot. http://qa.drupal.org/pifr/test/26666
Comment #31
Scott Reynolds commentedin the log for form.test its syntax error was "No syntax errors detected in ./modules/simpletest/tests/form.test". So I tested in on my end as well with php -l -f
Strange. So Im just going to resubmit it and see what happens.
Comment #32
Scott Reynolds commentedComment #33
Scott Reynolds commentedthree failed tests
http://qa.drupal.org/pifr/test/26806
Can't help but feel like I hijacked this bad boy with a working Test passing patch and trying to hard to create a test around it....
Comment #34
rfay#31: form_rebuild_form_build_id-671184-31.patch queued for re-testing.
Comment #36
rfayI'm unable to recreate the failure that the testbot reports, so I'm going to resubmit this now that the bot is really back.
Comment #37
rfay#31: form_rebuild_form_build_id-671184-31.patch queued for re-testing.
Comment #38
Scott Reynolds commentedI was unable to recreate the test failures as well. Not sure whats going on.
Comment #39
rfayIt apparently fails the bot again (although it hasn't reported back yet), and it's the same failure.
It looks to me like this is either an environmental difference or a problem with the testbot. Does anybody have experience debugging this type of disconnect? I suppose that I can install a VM with PIFR and run the test. Any other suggestions? I hate to have an obscure problem like this derail this issue.
Comment #41
rfayOK, boombatower helped me with this. The problem was that the test assumed that the url would be "/form-test/form-action", but on the bot, these things run in a subdirectory, "checkout". So the test was missing a url():
We can hope that this one will pass :-)
Comment #42
Scott Reynolds commentednice! boombatower++ rfay++
Moving to RTBC per #23
Comment #43
sunComing back to this issue after some time and looking at this snippet, my first thought was: WTF? ;)
Let's add an inline comment here to explain the rotation.
FormCacheActionTest
Wrong indentation here.
This should not happen.
Powered by Dreditor.
Comment #44
pasqualleComment #45
rfayPasqualle's patch does exactly the cosmetic fixes requested by Sun and adds no other complexity or concerns.
Moving it once again back to RTBC. This is the same functionality approved 4 times already.
Comment #46
okday commentedsubscribing
Comment #47
Scott Reynolds commentedSeems like that might be one small error. Function form_test_action_persist doesn't actually return an array. Ironically, thats not a problem as the form still gets cached.
Comment #48
fagoThe fix makes it impossible for a form constructor to just change the #action during rebuild - that's bad. Also copying around the action that way is quite confusing and worse now the action is in $form and $form_state, so developers might be confused which one has to be changed/used.
Instead why not just prevent the #action from being overwritten when it shouldn't? When the rebuild in ajax_form_callback() sets the #action to request_uri() write back the old value and we are done. That way we don't have confusing multiple values and devs are able to change the #action easily.
Comment #49
Scott Reynolds commentedOk well lets see how the bot likes this. This approach I think addresses fago's concerns by only overriding $form['#action'] on ajax callbacks.
The problem that is happening is the cache_form table gets a form saved into it that has the wrong #action on ajax forms. So to address this problem, this patch introduces a ajax_rebuild_form() which is similar to drupal_rebuild_form() except it requires a $form_action string and a $form_build_id string.
This patch also changed drupal_rebuild_form() to remove all the AJAX stuff from it. Kindof a nice cleanup.
This needs a test though, so first just seeing how the bot likes this.
Comment #51
Scott Reynolds commented#49: rebuild_form_671184-49.patch queued for re-testing.
Comment #52
Scott Reynolds commentedThis patch introduces a test around ajax_rebuild_form(). After writing this test, not sure it is particularly useful. I couldn't come up with another way to test it without actually calling ajax_rebuild_form directly.
Comment #54
Scott Reynolds commented#52: rebuild_form_671184-52.patch queued for re-testing.
Comment #55
effulgentsia commentedTagging. Thanks for all the great work on this. I'll try to review this this week, but if others get it to RTBC before then, please don't wait on my account.
Comment #56
fago+1 having a separate ajax_rebuild_form(), makes completely sense.
However having $form_action as a separate argument looks a bit unlogical to me - I think it would be cleaner if we just pass the previous form to the function. Also the build id can be set to the usual $form['#build_id'] by ajax_get_form() and the form_id is already there - that way we could just pass the previous form and $form_state ?
Apart from that:
* The comment of ajax_rebuild_form () talks about AHAH - we should consistently use AJAX.
* What is a "csid" ? I guess that should be "cid". Anyway "cache id" would be probably easier to read ;)
* The comment should note what's difference to drupal_rebuild_form().
There shouldn't be any other contexts left - but anyway we should keep that just to be sure. Thus the comment should say so.
@test:
There are already some AJAX related tests, see AJAXFormValuesTestCase. I think doing it analogously, thus invoking a drupalPostAJAX() and then checking the #action of the cached form should do it.
Comment #57
Scott Reynolds commentedYa csid is carried over from the rebuild form. I will update both spots.
I not sure I agree with this. By what metric is it cleaner? Passing in two separate arguments it makes it clearer what the form is going to do. Its going to create a form from the $form_id, set the form's action to the passed in variable and then cache the form away with the supplied form build id.
Being separate makes the documentation clear and the code itself easier to read and understand.
Comment #58
fagohm, I wasn't sure about that either. However just passing the action as now is unclear - which form action should that be and why should one pass that? So at least we could improve the docs, but I'd prefer if the caller of that function wouldn't have to care about that at all.
Anyway, another possible way to go would be to read it from $form_state['complete form'], which is only available if the form has been processed previously, but that should be the case when someone does 'rebuilding'.
Comment #59
Scott Reynolds commentedWhy? Thats the whole point to address your comments in #48. The caller is the function that processes the ajax response. That is the single entity that is responsible for knowing what form was posted and what the next steps should be.
Comment #60
s3ndal3 commented#41: drupal.system_ajax_inappropriate_671184_41.patch queued for re-testing.
Comment #61
Scott Reynolds commented... wrong patch to test, but we already determined that this patch needs work.
Comment #62
fagoad #59:
Yes, of course. But that doesn't mean the developer calling ajax_rebuild_form() has to think about that and has to explicitly pass the form action. So all I say is it would be nice if that would work automatically, or if not it needs at least better docs. Either way is fine for me as it keeps changing $form['#action'] during a regular form rebuild working (= #48).
Comment #63
Scott Reynolds commentedI ended up deciding I agree with you actually. The developer calling ajax_rebuild_form() calls it by saying "Rebuild this form, with this build id and here is the previous built form" and I think that makes sense.
Comment #64
fagoGreat! But still we need to comment why we do this:
+ $form['#action'] = $old_form['#action'];
Also we can read the build_id out of $old_form, so we don't have to pass that separately.
We could do the same way for $form_id, but I think it makes sense to have consistent parameters for the API functions: ($form_id, $form, $form_state) ? That way we could also remove the build id from the returned list of ajax_get_form().
Comment #65
pribeh commentedsubscribing.
Comment #66
rfayLet's get this one finished and landed. It's just way too annoying.
Comment #67
sunMissing () after function names.
Should refer to 'trigger_element', not 'clicked button'.
I don't think we need to duplicate the entire docs of drupal_rebuild_form(). The first sentence is sufficient.
1) $old_form should clarify why it is needed.
2) Missing blank line before @return.
Missing trailing period.
We should now add a note to the function's PHPDoc that drupal_rebuild_form() should only ever be called if 'rebuild' is TRUE.
The signature of drupal_rebuild_form() is changed here, but functions calling drupal_rebuild_form() are not updated accordingly.
133 critical left. Go review some!
Comment #68
Scott Reynolds commentedNot true actually. drupal_rebuild_form is called sparingly and the three places it is called it has been updated. File_upload_ajax, ajax_callback, and drupal_rebuild_form (which doesn't use the now removed arguments).
This patch addresses all but
Cause Im not sure that is true. Please explain.
Comment #69
sunThanks. Right, I was mistaken about drupal_build_form().
I'll let effulgentsia comment on the "do not invoke drupal_rebuild_form() without 'rebuild' == TRUE" WTF factor, as he's much more familiar with that portion of the code ;)
Comment #70
fagoAt the beginning of drupal_rebuild_form() there is a comment explaining that:
Comment #71
sunMy point is that - since AJAX uses ajax_rebuild_form() now - I don't know of any "other context" that invokes drupal_rebuild_form() outside of a true rebuild scenario.
Comment #72
rfayLooks like the test(s) were lost out of this version.
Comment #73
effulgentsia commentedIn this patch, ajax_rebuild_form() no longer takes a $form_build_id parameter.
Overall the patch looks great. From what I can gather, the key bug-fix here is to realize that not only $form_build_id, but also #action need to be carried over from the previous $form during a rebuild, so $previous_form rather than just the build id need to be passed to the rebuild function. I think that is a great improvement. However, I don't understand why we want ajax_rebuild_form() to be a different function than drupal_rebuild_form() (sorry if I missed the answer to that in a previous comment, please point me to the appropriate comment). They're exactly the same except for carrying over #build_id and #action, so why can't drupal_rebuild_form() take an optional $previous_form parameter and carry over those properties if that parameter is passed?
With respect to the code comment in HEAD that says "AJAX and other contexts...", the other "contexts" I was thinking of when I wrote that was something like a Flash interface, which would be similar to AJAX in that it can update part of the page/screen instead of having to do a full page reload. I don't think that most web developers would call a Flash interface AJAX, but it does seem like in Drupal, we use the word AJAX a little more broadly, and at least within FAPI, consider any scenario that returns a part of the form instead of the full form as AJAX. Would a Flash interface submit to "system/ajax"? Would the server-side portion of the module use #ajax? Would it create its own flash_form_callback() function that's different than ajax_form_callback(), but then call ajax_rebuild_form()? Don't know: we'll need to wait for a contrib module to figure these things out.
Comment #74
cdale commentedI may be off base here, but I noticed this when making my own D6 AJAX callbacks, that rebuilding the form when there are validation errors could cause strange things to happen.
For example, if I have a form that checks the value of a field to determine how it should be rebuilt, and another field which also is used to help me rebuild the form, and I mark both of these fields as required, but one is not filled in, on a normal (non-ajax) build, we will get some form errors, and the form will not be rebuilt, but will simply be re-rendered as it was from the cache, with the fields with errors being flagged.
In the current ajax flow, if there is a validation error, the message will be displayed, but the form will also be rebuilt, which may cause UI problems when the rebuild removes the field that had the validation error. Not to mention that in the event of an error, submit callbacks are not usually called, so we may get an inconsistent state in the rebuild, by the rebuild expecting certain flags to be set.
The normal form flow does not rebuild the form if there are form errors, should AJAX submits also be the same?
From form.inc, in drupal_build_form():
Am I missing something here? Is it up to the module author to check for errors and not rebuild if there are? My understanding of AJAX forms is that they should be handled like a normal multi-step form, and if the user wants a rebuild, they should set that on the form state in there submit callback, which is only called where there are no validation errors.
I'm just wondering why the AJAX process has been made to work differently than a normal form process in regards to rebuilding when we have form errors, and the explicit forcing of a "rebuild" when it might not be right to do so.
Please let me know if I am off base here, its just one of the things I've noticed that has puzzled me a little, and I'm wondering what I'm missing.
Comment #75
effulgentsia commented@cdale: good point. I opened a separate issue for discussing that: #756762: AJAX should follow same rules for whether to call drupal_rebuild_form() as non-AJAX submissions.
Comment #76
effulgentsia commentedSorry if I'm hijacking this, but here's my recommendation based on #73. I added a test that performs the steps in the OD and confirmed that it fails in HEAD and passes with the patch. I looked though the tests of prior patches, but I'm not sure what's still relevant from there. Please add the ones needed, if any.
Also tagging with "API change", which both #68 and this one include. But this is a critical bug-fix, so it's necessary. Still, any contrib modules out there that already implemented their own AJAX callback that does not use ajax_form_callback() will need to be notified about the change to the 3rd parameter in drupal_rebuild_form(). If nothing else, at least an email to the dev list is in order.
Comment #77
sunThanks! I agree with this simplification.
Hm. Normally, this would have a better home in FormsRebuildTestCase in form.test. But I see you used AJAXTestCase helper methods...
Also not sure why $old_form was renamed to $previous_form; I liked the former.
But anyway, nothing to hold up this critical fix.
131 critical left. Go review some!
Comment #78
Scott Reynolds commentedThe comments talked about 'previous form' when it used $old_form. That is the only rationale.
The reason I created a similar function to drupal_rebuild_form is because I felt that drupal_rebuild_form was overloaded and did things in two modes, ajax and non-ajax. And by separating things out, it cleaned up documentation (no more "If this is passed in then it does that.." and "ajax context this, normal context that"). And! it allows for further fixes for just rebuilding ajax forms, (i.e. HTML ids would be one example where the form would need to behave differently when rebuilt.)
Comment #79
dries commentedWhen I read the above paragraph, it wasn't clear what the 'previous build' is. Is it the $form_state? Is it the form definition? Is it the submitted form? What format is it in, and how does one obtain the 'previous build'. I think that could be clarified.
It would also be good to explain 'why' we need to do that. It looks like it is trying to explain why, but it is still quite abstract.
Comment #80
icecreamyou commentedTracking.
I've struggled with this before, unaware that it was a core bug. It affects modules that interact with Facebook-style Statuses.
Comment #81
effulgentsia commentedWith #77 and #79.
Comment #82
sun"Slightly" optimized the comments.
Comment #83
dries commentedLooks good. Committed to CVS HEAD.
Comment #84
rfayYeah, nice work! Thanks for all the great effort.
Setting to D6, as this has haunted D6 forever, history in #484838: [rfay version] AJAX form can submit inappropriately to system/ajax after failed validation. It's a big difference there, though. We could do one of the cheap fixes, perhaps even the little one in http://drupal.org/node/484838#comment-2512712.
Comment #85
effulgentsia commentedPlease see #756762: AJAX should follow same rules for whether to call drupal_rebuild_form() as non-AJAX submissions for follow-up improvements.
Comment #86
willvincent commentedCouldn't figure out how to backport this to d6, with the significant changes between versions, but, the small fix mentioned in http://drupal.org/node/484838#comment-2512712 does seem to fix the issue (at least for me) when applied manually.
Line #167 in system.module has to change from:
to:
As stated in that comment, this just removes the assertion that the action should be the request_uri, which apparently causes the form submit button to submit to the current url, which is the desired behavior.
I'd submit a patch, but for whatever reason I can never get a patch to pass the bot test.
Comment #87
rfayThis is still annoying in D6, so we should probably get it fixed. As I remember, chx doesn't approve of the approach in #484838-7: [rfay version] AJAX form can submit inappropriately to system/ajax after failed validation. However, the slightly fancier approach in this issue should be do-able.
Comment #88
Scott Reynolds commented@#86 I believe that change will break things.
Just put make a patch with name_of_patch-D6.patch and the testbot won't try to test it. As testbot should ignore D6 patches.
Comment #89
bflora commentedAny ever go through with porting this back to D6?
Comment #90
andypostAnother follow-up #1414510: Remove drupal_reset_static for drupal_html_id() when form validation fails
Comment #91
johnnydarkko commented#14: form_rebuild_form_build_id-671184-14.patch queued for re-testing.
Sorry... got trigger-happy with the link clicking.
Comment #92
charlie-s commentedTo others that are still encountering this error and stumble upon this issue, it's critical that you pass the old form into drupal_rebuild_form() as the 3rd argument to get the form rebuilt as expected:
Comment #94
panchoFixed in 7.x, just not backported to 6.x, see #83.