Download & Extend

ajax_html_ids are broken for forms with file element (encoding=multipart/form-data)

Project:Drupal core
Version:7.x-dev
Component:ajax system
Category:bug report
Priority:normal
Assigned:Unassigned
Status:reviewed & tested by the community
Issue tags:Ajax, frontend performance, JavaScript, jQuery, needs backport to D7, Needs committer feedback, Needs JS testing

Issue Summary

Currently when form has file element there's no ability to send ajax_html_ids because it's not possible to serialize that array to send with jQuery Form plugin shipped with core.

Also ajax_html_ids has array structure that makes ajax requests take more bandwidth by prefixing each value with

ajax_html_ids[]

.

Proposed solution

Send ajax_html_ids serialized imploded with space because HTML5 standard does not limits allowed characters for HTML IDs except 'white-space'.

The ajax_page_state already has plain structure so this is some kind of standardization for ajax variables.

Also this change help to minimize request size for ajax calls as discussed in #956186: [meta] Allow to use AJAX with GET requests

Testing troubles

Currently simpletest has no ability to test ajax request properly so only manual testing possible.

Steps to reproduce

- download and enable attached module (ajaxdemo)

- visit /ajaxdemo_test_html_ids and press Get
-- bellow will output count and list of used IDs (they are passed as array)

- visit ajaxdemo_test_html_ids/1 and press Get (file element should appear on form)
-- there would be only one element serialized with ',' (in this case core can't extract IDs used on page)

Related #1574560: Update jQuery Form Plugin to the latest in the git repository

Comments

#1

Proposed patch

Also attached a module to test

Screens of module
-- without file element $ids are passed as array
without file element

-- with file element $ids are broken
file element breaks

AttachmentSizeStatusTest resultOperations
1575060-htmlid.patch2.54 KBIdlePASSED: [[SimpleTest]]: [MySQL] 36,581 pass(es).View details | Re-test
ajaxdemo.tar_.gz745 bytesIgnoredNoneNone
no_file.png9.08 KBIgnoredNoneNone
with_file.png18.63 KBIgnoredNoneNone

#2

Tagging, suppose this does not break any existing code so could be backported

Also there's a lot of troubles could be fixed with this small change, forums has a lot of posts when ajax calls print garbage on screen.

PS: found when working on #1414510-41: Remove drupal_reset_static for drupal_html_id() when form validation fails

#3

Component:javascript» ajax system

+1 much better. good to go for me.

I'll let the ajax maintainers have a look.

#4

On vanila D8 install data for ajax_html_ids reduced twice

befora patch ajax_html_ids%5B%5D=overlay-profile-link&ajax_html_ids%5B%5D=overlay-dismiss-message&ajax_html_ids%5B%5D=toolbar&ajax_html_ids%5B%5D=toolbar-home&ajax_html_ids%5B%5D=toolbar-user&ajax_html_ids%5B%5D=toolbar-menu&ajax_html_ids%5B%5D=toolbar-link-admin-dashboard&ajax_html_ids%5B%5D=toolbar-link-admin-content&ajax_html_ids%5B%5D=toolbar-link-admin-structure&ajax_html_ids%5B%5D=toolbar-link-admin-appearance&ajax_html_ids%5B%5D=toolbar-link-admin-people&ajax_html_ids%5B%5D=toolbar-link-admin-modules&ajax_html_ids%5B%5D=toolbar-link-admin-config&ajax_html_ids%5B%5D=toolbar-link-admin-reports&ajax_html_ids%5B%5D=toolbar-link-admin-help&ajax_html_ids%5B%5D=shortcut-toolbar&ajax_html_ids%5B%5D=edit-shortcuts&ajax_html_ids%5B%5D=page-wrapper&ajax_html_ids%5B%5D=page&ajax_html_ids%5B%5D=header&ajax_html_ids%5B%5D=logo&ajax_html_ids%5B%5D=name-and-slogan&ajax_html_ids%5B%5D=site-name&ajax_html_ids%5B%5D=main-menu&ajax_html_ids%5B%5D=main-menu-links&ajax_html_ids%5B%5D=secondary-menu&ajax_html_ids%5B%5D=secondary-menu-links&ajax_html_ids%5B%5D=main-wrapper&ajax_html_ids%5B%5D=main&ajax_html_ids%5B%5D=sidebar-first&ajax_html_ids%5B%5D=block-search-form&ajax_html_ids%5B%5D=search-block-form&ajax_html_ids%5B%5D=edit-search-block-form--2&ajax_html_ids%5B%5D=edit-actions&ajax_html_ids%5B%5D=edit-submit--2&ajax_html_ids%5B%5D=block-system-navigation&ajax_html_ids%5B%5D=content&ajax_html_ids%5B%5D=main-content&ajax_html_ids%5B%5D=page-title&ajax_html_ids%5B%5D=block-system-main&ajax_html_ids%5B%5D=ajaxdemo-html-ids-form&ajax_html_ids%5B%5D=edit-submit&ajax_html_ids%5B%5D=message_area&ajax_html_ids%5B%5D=footer-wrapper&ajax_html_ids%5B%5D=footer&ajax_html_ids%5B%5D=block-system-powered-by

after patch ajax_html_ids=skip-link+overlay-disable-message+overlay-profile-link+overlay-dismiss-message+toolbar+toolbar-home+toolbar-user+toolbar-menu+toolbar-link-admin-dashboard+toolbar-link-admin-content+toolbar-link-admin-structure+toolbar-link-admin-appearance+toolbar-link-admin-people+toolbar-link-admin-modules+toolbar-link-admin-config+toolbar-link-admin-reports+toolbar-link-admin-help+shortcut-toolbar+edit-shortcuts+page-wrapper+page+header+logo+name-and-slogan+site-name+main-menu+main-menu-links+secondary-menu+secondary-menu-links+main-wrapper+main+sidebar-first+block-search-form+search-block-form+edit-search-block-form--2+edit-actions+edit-submit--2+block-system-navigation+content+main-content+page-title+block-system-main+ajaxdemo-html-ids-form+edit-submit+message_area+footer-wrapper+footer+block-system-powered-by

#5

Issue tags:-jQuery, -jQuery UI, -Performance

For file uploads, jQuery Form and our Ajax framework are using an iframe containing a textarea element that contains the embedded JSON returned from the server.

See http://api.drupal.org/api/drupal/core!includes!ajax.inc/function/ajax_deliver/8

If the HTML IDs are broken, then other returned JSON values are possibly broken, too. We should check for potential JSON encoding and decoding differences instead.

Updating jquery.form.js to a later version might also resolve the issue.

#6

Issue tags:+jQuery, +Performance

@sun this issue about frontend and how jQuery Form serializes data before send (see ajax.js# Drupal.ajax.prototype.beforeSend )

As I written ajax_html_ids does not send to Drupal when form has encoding=multipart/form-date

I leave a performance tag for reasons of optimization POSTed data

There's already #1574560: Update jQuery Form Plugin to the latest in the git repository

EDIT if jQueryForm get updated to latest version (which uses JS strict) then ajax submission form with multipard encoding would fail with error:
An error occurred while attempting to process /system/ajax: Component returned failure code: 0x80460001 (NS_ERROR_CANNOT_CONVERT_DATA) [nsIDOMFormData.append]

#7

Title:drupal_html_id() broken for forms with file element» ajax_html_ids are broken for forms with file element (encoding=multipart/form-data)
Issue tags:-Performance+frontend performance

re-titled

#8

re-roll chasing core

AttachmentSizeStatusTest resultOperations
1575060-htmlid-8.patch2.54 KBIdlePASSED: [[SimpleTest]]: [MySQL] 39,904 pass(es).View details | Re-test

#9

#10

Regarding #6, there's a pull request at https://github.com/malsup/form/pull/201/files

#11

not sure it's possible to backport to D7 as is but we can make a check to make sure that html_ids are passed as array and in cahe of string just decode them back to array

#12

#8: 1575060-htmlid-8.patch queued for re-testing.

#13

Re-roll to chase core.

AttachmentSizeStatusTest resultOperations
1575060-htmlid-13.patch2.54 KBIdlePASSED: [[SimpleTest]]: [MySQL] 41,105 pass(es).View details | Re-test

#14

#13: 1575060-htmlid-13.patch queued for re-testing.

#15

Status:needs review» reviewed & tested by the community

Let's get this commited before feature freeze

#16

Status:reviewed & tested by the community» needs work

Can we reroll using querySelectorAll please?

35% faster is kinda significant since we have so many ids on a page usually.

http://jsperf.com/drupal-id-selector/2

(edit) Turns out there was a problem in my test, andypost fixed it, thanks :) new link added.

#17

Status:needs work» needs review
Issue tags:+Needs manual testing

Re-roll for qSA a lot of tests could be found in http://jsperf.com/drupal-id-selector/2

AttachmentSizeStatusTest resultOperations
1575060-htmlid-17.patch2.64 KBIdlePASSED: [[SimpleTest]]: [MySQL] 41,106 pass(es).View details | Re-test

#18

Status:needs review» reviewed & tested by the community

tested and works very well :)

I changed the way we access ajax_html_ids, JSHint was complaining. I wasn't too happy about changing the type of ajax_html_ids so I just used another variable, no functional change, just moving things around :)

AttachmentSizeStatusTest resultOperations
core-js-ajaxhtmlids-1575060-18.patch2.59 KBIdlePASSED: [[SimpleTest]]: [MySQL] 41,104 pass(es).View details | Re-test

#19

Issue tags:-Needs manual testing

forgot tag

#20

+++ b/core/misc/ajax.jsundefined
@@ -284,10 +284,13 @@ Drupal.ajax.prototype.beforeSerialize = function (element, options) {
+  var ajaxHtmlIds = [];

Great! This should be a bit faster then access to array element

#21

I guess it's ok to go with this for now (since we need to backport it), but we should really try to get the PR in #10 in the upstream library, so that we can remove this workaround.

That is, because this workaround is a one-off solution for particular extra data we're sending, and it's perfectly possible that there are contrib and custom modules affected by this jQuery Form bug, too.

However, one last nit: The variable dump in #1 showed commas as separators, whereas the latest patch uses spaces. The HTML spec does not allow for commas in IDs, and passing a list using commas as delimiter appears much more natural to me (and also avoids URI escaping of spaces), so I wonder whether we could change the delimiter back to commas?

#22

@sun, about the space, thought that as well. Turns out, ",", ":" and some other weird characters are allowed in ids. Only the space is explicitly forbidden by the spec. http://www.w3.org/TR/html-markup/global-attributes.html#common.attrs.id

The nice benefit is how much the request size shrinks too.

#23

drupal_html_id() documents
- http://www.w3.org/TR/html4/types.html#type-name
- http://www.w3.org/TR/CSS21/syndata.html#characters

as the main constraints for HTML IDs. HTML5 is less restrictive, and this use-case is different, so hm, it's probably fine to diverge from drupal_html_id(). (though I wonder whether it wouldn't be helpful for future reference and readers of ajax.js to have an inline comment in there similar to the one in drupal_html_id() that explains the situation)

#24

Status:reviewed & tested by the community» needs review
Issue tags:+Needs JS testing

We talked at length in IRC about large refactoring patches like this, and their likelihood to break things in core. We decided to tag issues like this with "Needs JS testing" and then zendoodles and her Merry Band of Contributors will test these patches manually (for now) and with automated testing (later!).

Tagging so she can find it. :)

#25

#10 just got fixed upstream

#26

New version contains the fix, see http://malsup.github.com/jquery.form.js

#27

Followed instructions in issue summary for manual testing. I'm getting same results with patch applied and not applied:

See screenshots attached.

AttachmentSizeStatusTest resultOperations
Patch-NotApplied-NoFile.png28.58 KBIgnoredNoneNone
Patch-NotApplied-File.png31.4 KBIgnoredNoneNone
Patch-Applied-File.png32.11 KBIgnoredNoneNone
Patch-Applied-NoFile.png28.84 KBIgnoredNoneNone

#28

Status:needs review» reviewed & tested by the community

After checking up on this issue I can confirm that the behaviour described in the issue summary is correct, and that the patch works as intended.

I did additional testing on AJAX requests in Core and couldn't find anything broken by this patch. I couldn't find anything special in core that seems to rely on $_POST['ajax_html_ids'] that would be impacted by this patch.

#29

Version:8.x-dev» 7.x-dev
Status:reviewed & tested by the community» patch (to be ported)

Thanks for the manual testing. Committed/pushed to 8.x.

#30

Status:patch (to be ported)» needs work

For D7 we can't use document.querySelectorAll() so patch should use fastest approach from testing #17,
also we should file a change-notice about browser compatibility and new format of ajax-html-ids

#31

Re-roll for D7

AttachmentSizeStatusTest resultOperations
1575060-patch-d7.patch1.51 KBIdleFAILED: [[SimpleTest]]: [MySQL] 39,595 pass(es), 18 fail(s), and 6,252 exception(s).View details | Re-test

#32

Status:needs work» needs review

#33

Sorry. Added simpletest to patch.

AttachmentSizeStatusTest resultOperations
1575060-patch-d7.patch2.47 KBIdlePASSED: [[SimpleTest]]: [MySQL] 39,391 pass(es).View details | Re-test

#34

Status:needs review» reviewed & tested by the community
Issue tags:+Needs committer feedback

We need David Rothstein confirmation on new format of data, or we just proceed with tremendous array for D7

[00:41] andypost: webchick: you have point about BC for http://drupal.org/node/1575060 I think this feature very internal and we can to send a minified version of IDs, are you agree?
[00:42] Druplicon: http://drupal.org/node/1575060 => ajax_html_ids are broken for forms with file element (encoding=multipart/form-data) => Drupal core, ajax system, normal, needs work, 30 comments, 6 IRC mentions
[00:42] webchick: andypost: hm. i trust David_Rothstein on those types of questions

#35

Assigned to:Anonymous» David_Rothstein

Making it official. :)

#36

Assigned to:David_Rothstein» Anonymous
Status:reviewed & tested by the community» needs review

If this comment from #11 is correct:

not sure it's possible to backport to D7 as is but we can make a check to make sure that html_ids are passed as array and in cahe of string just decode them back to array

then that sounds like a much better way to fix this issue in Drupal 7 to me, rather than changing $_POST['ajax_html_ids'] for everyone.

For example, I just did a grep through a large codebase of an actual Drupal 7 site and found all sorts of modules doing things with $_POST['ajax_html_ids'] directly. These included Panels, Ctools, Views, etc. Although most didn't look like they were doing much with it except forcefully unsetting it (or resetting it to an empty array) in certain conditions, and I didn't look too far in depth into what they were doing, that's enough to give me pause about the idea of changing this from an array to a string in the middle of a stable release, especially if we don't have to.

#37

Just wanna to mention about unset used in contrib - this all caused by 2 bugs in core - this one and #1414510: Remove drupal_reset_static for drupal_html_id() when form validation fails
So contrib modules can't use this data because it could be easily broken (element IDs are changing) so this leads to #1305882: drupal_html_id() considered harmful

For D7 I see only one solution - implement a check for $_POST['ajax_html_ids'] that it is a string or array and in case on string make it array

#38

#39

I came across this bug when building a module that enables you to create entities via ajax. The #33 D7 patch seems to fix the problem for me. Thanks guys.

#40

Status:needs review» reviewed & tested by the community

I can confirm that the patch in #33 works and is badly needed when doing complex #ajax stuff. Let's please get this in 7 core.

#41

Status:reviewed & tested by the community» needs review

I think we can simply check recieved data and convert it into array if we got string.
Patch attached.

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-41.patch2.03 KBIdleFAILED: [[SimpleTest]]: [MySQL] 39,818 pass(es), 17 fail(s), and 0 exception(s).View details | Re-test

#42

Status:needs review» needs work

The last submitted patch, drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-41.patch, failed testing.

#43

Status:needs work» needs review

Ops, my bad. New patch attached.

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-43.patch2.03 KBIdlePASSED: [[SimpleTest]]: [MySQL] 39,807 pass(es).View details | Re-test

#44

Status:needs review» reviewed & tested by the community

Because the contrib could have own fixes for the issue we should accept #43

While I think the the proper fix is #33 but a bit progressive as David pointed in #36 most of contrib just unset($_POST['ajax_html_ids']) to recalculate IDs

#45

This suits me—as long as those ids get converted to an array I'm happy and our module will be actually usable: Entity Dialog

#46

Closely related issue #799356: _form_set_class() is too aggressive in assigning the 'error' class needs to be commited after this change

+++ b/includes/common.incundefined
@@ -3878,7 +3878,15 @@ function drupal_html_id($id) {
+        // For some reason (http://drupal.org/node/1575060) jquery.form.js can send string instead of array.

the line exceeds 80 chars, so here's re-roll

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-46.patch2.04 KBIdlePASSED: [[SimpleTest]]: [MySQL] 40,323 pass(es).View details | Re-test

#47

Thanks @andypost!

Re-rolled patch with more clear comment.

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-47.patch2.12 KBIdlePASSED: [[SimpleTest]]: [MySQL] 40,206 pass(es).View details | Re-test

#48

+++ b/modules/simpletest/drupal_web_test_case.phpundefined
@@ -2154,9 +2154,12 @@ class DrupalWebTestCase extends DrupalTestCase {
-      $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id);
+      $ajax_html_ids[] = (string) $element['id'];
+    }
+    if (!empty($ajax_html_ids)) {
+      $extra_post .= '&' . urlencode('ajax_html_ids') . '=' . urlencode(implode(',', $ajax_html_ids));

Not sure that tests should work with wrong format. Let's revert this part or add a condition

#49

Status:reviewed & tested by the community» needs work

Agreed, I don't think we should make that kind of change to a fundamental test method like drupalPostAJAX() as part of this patch... that should really use the expected format. So, "needs work"?

(I'm not sure if it's worth a D7 test or not though, but if so we should use a separate method or do it with a conditional parameter as @andypost suggested.)

Also while we're at it:

+        // jquery.form.js may send to the server a string instead of an array
+        // because of specific reasons (see http://drupal.org/node/1575060).
+        // In this case we need to convert a received string into an array
+        // in order to keep to the default behavior.

Those "specific reasons" aren't very specific :) How about:

jquery.form.js may send the server a comma-separated string instead of an array (see http://drupal.org/node/1575060), so we need to convert it to an array in that case.

#50

Status:needs work» needs review

Re-rolled patch with new comments.

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-50.patch2.04 KBIdlePASSED: [[SimpleTest]]: [MySQL] 40,305 pass(es).View details | Re-test

#51

+++ b/modules/simpletest/drupal_web_test_case.phpundefined
@@ -2154,9 +2154,12 @@ class DrupalWebTestCase extends DrupalTestCase {
+    $ajax_html_ids = array();
     foreach ($this->xpath('//*[@id]') as $element) {
-      $id = (string) $element['id'];
-      $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id);
+      $ajax_html_ids[] = (string) $element['id'];
+    }
+    if (!empty($ajax_html_ids)) {
+      $extra_post .= '&' . urlencode('ajax_html_ids') . '=' . urlencode(implode(',', $ajax_html_ids));

suppose this part better to drop for D7.
Then we need to revisit this for D8 once the issue no more reproducible after upgrade of form library

#52

Droped test part.

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-52.patch1.08 KBIdlePASSED: [[SimpleTest]]: [MySQL] 40,306 pass(es).View details | Re-test

#53

Status:needs review» reviewed & tested by the community

Just removed simpletest hunk
For D7 we just need to fix bug

Sorry for crosspost

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-52.patch1.08 KBIdleFAILED: [[SimpleTest]]: [MySQL] Setup environment: Test cancelled by admin prior to completion.View details | Re-test

#54

Status:reviewed & tested by the community» needs work

The last submitted patch, drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-52.patch, failed testing.

#55

Status:needs work» reviewed & tested by the community

Magically stopping the bot breaks status

#56

Status:reviewed & tested by the community» fixed

Committed to 7.x - thanks! http://drupalcode.org/project/drupal.git/commit/005708c

#57

Version:7.x-dev» 8.x-dev
Status:fixed» needs work

According #51, #21 we need to update D8 library to fixed version with commit https://github.com/malsup/form/pull/201/files
Also issue needs change notice about D8 ajax_html_id which now a string
Related issue added #1574560: Update jQuery Form Plugin to the latest in the git repository

#58

Version:8.x-dev» 7.x-dev
Status:needs work» needs review

The fix in #56 did not resolve the issue. When I download and install ajaxdemo from #1 and test it with Drupal 7, I get

count $_POST[ajax_html_ids] = 1
$_POST[ajax_html_ids] = array (
  0 => 'skip-link,toolbar,toolbar-home,toolbar-user,toolbar-menu,toolbar-link-admin-dashboard,toolbar-link-admin-content,toolbar-link-admin-structure,toolbar-link-admin-appearance,toolbar-link-admin-people,toolbar-link-admin-modules,toolbar-link-admin-config,toolbar-link-admin-reports,toolbar-link-admin-help,edit-shortcuts,page-wrapper,page,header,logo,name-and-slogan,site-name,main-menu,main-menu-links,secondary-menu,secondary-menu-links,main-wrapper,main,breadcrumb,sidebar-first,block-search-form,search-block-form,edit-search-block-form--2,edit-actions,edit-submit--2,block-system-navigation,content,main-content,page-title,block-system-main,ajaxdemo-html-ids-form,edit-file,edit-submit,message_area,footer-wrapper,footer,block-system-powered-by',
)

The fix in #56 only works when $_POST[ajax_html_ids] = 'skip-link,toolbar, ... ' which is not the case

Attached is a patch that rectify this. Again, this can only be manually tested.

AttachmentSizeStatusTest resultOperations
1575060-58-ajax_html_ids.patch1.25 KBIdleFAILED: [[SimpleTest]]: [MySQL] 40,373 pass(es), 17 fail(s), and 0 exception(s).View details | Re-test

#59

Status:needs review» reviewed & tested by the community

Tested with module #1

+++ b/includes/common.incundefined
@@ -3888,14 +3888,14 @@ function drupal_html_id($id) {
+      if (strpos($_POST['ajax_html_ids'][0], ',') !== FALSE) {

=== FALSE - means no comma found

AttachmentSizeStatusTest resultOperations
drupal-ajax_html_ids-are-broken-for-forms-with-file-element-1575060-59.patch1.29 KBIdlePASSED: [[SimpleTest]]: [MySQL] 40,339 pass(es).View details | Re-test

#60

Right, sorry for the mistake on the condition

nobody click here