When an AJAX settings command holds an empty JavaScript Array for the ajaxPageState.css setting, Drupal.settings.ajaxPageState.css is completely overwritten instead of merged.
Subsequent AJAX requests will hand an empty list of loaded stylesheets to the server, which thinks something went wrong and takes the easy way out by not including any stylesheets in the responses after that.
This was discovered while testing the lazy-loading patch for Wysiwyg module found in #356480-68: Lazy-load editors in an attempt to add custom content to a Panel node. The consequence was that TinyMCE got loaded without a custom stylesheet required by Wysiwyg module, making the editor's toolbar appear vertical instead of horizontal.
A similar situation is created when changing field display settings (clicking the cogwheel next to a field loads a few stylesheets the first time, but subsequent requests return an empty Array, overwriting Drupal.settings.ajaxPageState.css, but there seem to be no consequences since no other actions on that page requires further stylesheets to be lazy-loaded.)
To reproduce without visible consequences, D8 Core only:
- Edit any content type with fields, say Article.
- Click the "Manage Display" tab.
- Check the contents of
Drupal.settings.ajaxPageState.css, should hold all loaded stylesheets. - Click the cogwheel to configure a field's formatter. Notice how new stylesheets and scripts get loaded, and the request included all stylesheets in the POST data using
ajax_page_state[css][filename.css]. - Click Save or Cancel. Notice how no new styles are loaded, but the settings command executed as part of the AJAX response is given an empty list of stylesheets.
- Inspect
Drupal.settings.ajaxPageState.cssagain, it's now empty. - No AJAX request hereafter will include
ajax_page_state[css], tricking the server into thinking something went wrong (see ajax_render()).
To reproduce with visible consequences, D7 with contrib modules:
- Install and configure Panel Nodes, TinyMCE + Wysiwyg 7.x-2-x-dev and apply the patch mentioned above.
- Create a Panel node and save it.
- Click the "Panel Content" tab -> the cogwheel -> "Add content".
Notice that no new stylesheets or scripts are added via AJAX yet. (Use Firebug for Firefox or similar debugging tools.) - Click "new custom content" in the left column. Note how all required scripts, but no stylesheets, are downloaded.
- TinyMCE's toolbar gets rendered vertically instead of horizontally due to missing Wysiwyg stylesheets and the same
Drupal.settings.ajaxPageState.cssbehavior from the previous example can be seen.
The problem is Drupal responding with the JSON command containing command.settings.ajaxPageState.css = [], while we have Drupal.settings.ajaxPageState.css = {'file1.css' : 1, 'file2.css' : 1} (the first being an [empty] Array and the second an Object). The command implementation tries to merge these with jQuery.extend(true, Drupal.settings, settings); but it does not work as intended.
It does work when the AJAX response carries new css files with it because the Array will have non-numeric properties, making jQuery recognize and use it as a "plain Object".
Suggestions:
To get jQuery.extend() to work as we intend, always cast the stylesheet list to an object in drupal_get_css().
jQuery.extend() will then see that both the existing list and what to merge with it are Objects and not overwrite the existing list.
The list in Drupal.settings.ajaxPageState.js does not have this problem because the settings command is never given command.settings.ajaxPageState.js if it's empty, so there is no merge happening.
For consistency, why not skip command.settings.ajaxPageState.css if it's empty too?
Either (or both) changes will fix the problem, so I've attached two patches. Doing both seem a bit redundant in terms of just fixing the problem, but might be is good for clarity and future reference so I 'd consider that an option too merged both in #3.
| Comment | File | Size | Author |
|---|---|---|---|
| #51 | issue_5.jpg | 28.38 KB | pmos |
| #40 | 0001-Issue-1287368-by-TwoD-valthebald-xjm-sun-Fixed-Drupa.patch | 8.24 KB | sun |
| #31 | drupal8.ajax-page-state-css.31.patch | 7.75 KB | sun |
| #30 | ajax-page-state-css-test-1287368-30.patch | 7.76 KB | twod |
| #25 | ajax-page-state-css-test-1287368-25.patch | 7.76 KB | twod |
Comments
Comment #1
sunPossibly related:
#914792: Custom element properties entirely override default element info properties
#791860: array_merge_recursive() is never what we want in Drupal: add a drupal_array_merge_recursive() function instead.
#208611: Add drupal_array_merge_deep() and drupal_array_merge_deep_array() to stop drupal_add_js() from adding settings twice
Comment #2
twodAfter thinking some more about this, I'm adding the cast to the "skip"-patch as well, just to make sure jQuery or other code won't misinterpret it as an Array when we intend for it to be used as an Object.
So, new patch, this time without notices...
Comment #3
twodDoh...
Comment #4
bryancasler commentedsubscribe
Comment #5
rfayWow, @TwoD, thanks for the detailed report *and* solution!
Comment #6
jmones commentedsubscribe
Comment #7
xjmExcellent report and the patch looks sound. Let's get a couple manual testers confirming the patch resolves the issue and does not have unintended side effects.
Attached is a reroll for core/.
Comment #8
sunI wonder whether we can assert this in a unit test, too? Doesn't have to be a full AJAX interaction, testing the internal functions without any page loads should be fine.
Comment #9
valthebaldI have performed steps 1-7 as described in the issue
:
After performing step 4:
In original state (patch not applied)
After pressing 'Save' or 'Cancel' value of Drupal.settings.ajaxPageState.css is cleared
Patch fixes the issue
Just one tiny remark on steps to reproduce on D8:
1. Open 'display fields' screen directly, not in overlay (URL should be '/admin/structure/types/manage/article/display',
not '/#overlay=admin/structure/types/manage/article/display'. In the latter case Drupal.settings.ajaxPageState.css contains correct values both with and without patch.
my deep respect to the patch author for such a thorough description!
Comment #10
valthebaldComment #11
fonant commentedPatch in #7 works nicely with #356480-68: Lazy-load editors to fix CKeditor in panels in my Drupal 7.10 site.
Comment #12
pixelmord commentedSame here #7 patch works.
core 7.9
We ran into the same issue with forms in a modal window via a ctools content plugin.
The wysiwyg issue was fixed by http://drupal.org/node/356480
The issue, that attached CSS files were not loaded is fixed with the above mentioned patch.
Nice work, thanks!
Comment #13
twodThank you for the nice comments and help with testing!
I'll give writing my first unit tests a shot when I get home in a couple of hours, unless someone beats me to it.
Comment #14
valthebaldI have modified ajax.test (AJAXFormValuesTestCase class) to include simple checks either AJAX submit returns CSS/JS in case it was not changed by callback (empty CSS array is the bug's reason)
Comment #15
sunYour local git checkout is outdated and you diffed against origin/8.x. Either diff against 8.x, or make sure your local checkout is current.
Exceeds 80 chars. The comment could also be more clear about what exactly is being verified here.
Can be removed.
There is no function key_exists(); there is array_key_exists() only.
We should also test the second expectation that these are objects and not arrays somewhere.
14 days to next Drupal core point release.
Comment #16
valthebaldRerolled patch against latest 8.x.
I can't find better description of the added checks, any ideas?
Comment #17
valthebaldBy the way, I have a feeling that there is a bug in ajax.test:
several AJAX tests assume that consequent calls to drupalGetAjax()/drupalPostAjax() change return value of drupalGetSettings(), while what happens is that drupalGetSettings() remembers result returned by the first request, and does not update it during subsequent calls.
Have spent some time to find that.
Or am I missing something?
Comment #18
twodHow about a unit test to make sure the output of
drupal_get_css()anddrupal_get_js('header')match what we expect before and after adding stylesheets?Something like this?
EDIT: The first test fails without the #7-part of the patch and passes with it.
Comment #20
sunUm, isn't #7 already contained in the patch in #18? :)
Comment #21
twod#18: ajax-page-state-css-test-1287368-18.patch queued for re-testing.
Comment #22
twodYes, it is, what I meant was that if the part of #18 matching #7 is removed, the first new test fails as designed. The second test I added passes both with and without the part from #7, and I'm not sure if it's needed or not, but I did not find an existing test actually checking if ajaxPageState.js indeed contained the stylesheets (thought I added that comment in my EDIT above, but I guess not...).
Comment #24
valthebald#18: test of CS and/or JS added by AJAX callback is already done by AJAXFrameworkTestCase::testLazyLoad() (ajax.test).
The missing case is when AJAX callback does not add anything.
Comment #25
twodHmm, I wanted to test the direct outputs of
drupal_get_*to avoid any "interference" by other functions, but doing that in a unit test does not seem to be possible because it triggers database access both indrupal_alter()anddrupal_render(). Full webtests usedrupal_json_decode()to parse settings, which always returns an associative array, making it impossible to verifyDrupal.settings.ajaxPageState.cssis an object when it exists.Strangely enough, I can't reproduce the database error from the testbotlocally...
Anyway, why don't we put the "test for nothing"-case in AJAXFrameworkTestCase::testLazyLoad() (ajax.test) too?
In #16 the new tests run twice while the results will represent the same circumstances.
I kinda mixed what's in testLazyLoad with valthebald's array_key_exists check to see how it'd turn out. It won't verify the css list is an object, but that is of less concern as long as it's not empty.
Comment #26
valthebaldMay I mark this RTBC?
Comment #27
sunThanks! This looks good to me as well.
Comment #28
catchMainly nitpicks:
Arrays and objects aren't proper nouns, should be lower cased.
However CSS should be all upper-cased because it's an acronym.
Quick re-roll?
Comment #29
twodI'm at work right now, but will reroll when I get home.
I was referring to JavaScript's Array/Object types in that comment, but that might not have been clear enough.
Comment #30
twodMade the changes from #28.
Comment #31
sunThe change to "last" is bogus here; the files are still expected to be lazy-loaded as part of the next requests.
Additionally fixed that, back to RTBC.
Comment #32
legalfunding commentedThanks, great detailed report to.
Comment #33
twodI would have accepted "next requests", but the files aren't lazy-loaded in the first [new] request in the test. So "next request" just sounded wrong to me.
Comment #34
catchLast thing, is there an issue open for this already?
If not, can we open one?
Looks much better otherwise, so will commit once that's linked from this issue.
Comment #35
sunThere we go: #1392874: Add a drupal_css_defaults() helper function like drupal_js_defaults()
Comment #36
xjmWe can probably take the
@todoout then, right?Comment #37
sunNope, that's fine, as long as it's valid.
Comment #38
catchThanks! Committed/pushed to 8.x, moving to 7.x for backport.
Comment #39
twodGreat!
A simple
git cherry-pick 8cc5edeseems to be able to backport this to 7.x. Only had time to test AJAXFrameWorkTestCase yet though.Comment #40
sunComment #41
twodSince this patch is the same for D8 and D7, and the "'Page state still lacks the %css and %js files, as expected"-test correctly fails when the fix is left out, dare I say RTBC?
Comment #42
pdcarto commentedI came here from this wysiwyg issue - lazy load editors. I applied the patch (#40), cleared cache, and it works!
Comment #43
pdcarto commentedAm I correct that #40 has *not* been included in D7.12?
http://drupalcode.org/project/drupal.git/blob/7bb06635ee0fbb36d5e4b24368...
Comment #44
sunyeah, unfortunately not.
Comment #45
pdcarto commentedAnything we can do to improve the odds that it gets into the next release?
Comment #46
xjm@pdcarto -- The patch has already been committed to Drupal 8, the Drupal 7 backport is marked RTBC, and the next release of Drupal 7 won't be for another month. Therefore, in all likelihood, this patch will be included in the next release, unless something else comes up. (7.12 already included a lot of changes, which is why this and a handful of other issues didn't make it in time.)
A way to help might be to add issue summaries to this and other RTBC issues, which can help committers review the patches more easily so that they have time for more issues. :)
Comment #47
sunIf you'd like to see an issue fixed, please always make sure to help the maintainers and state:
Comment #48
webchickCool, thanks very much for the thorough bug report and test in this issue. I'm really sorry it didn't make it into 7.12, but hopefully #1423530: Help project maintainers manage RTBC queue on a first-in, first-out basis gets done; that'd certainly help with this kind of thing in the future.
Committed and pushed to 7.x. Thanks!
Comment #50
kristiaanvandeneyndeJust ran into this issue as well.
I'm glad to see it reported, fixed and committed instead of having to write a report myself :)
Nice job TwoD!
Comment #51
pmos commentedi would like to mention that after this update on drupal 7.14 having installed hierarchical select. After i make the selection of my hierarchical drop down, when an image is uploaded the style of the image field disappers. (see attached file). Wheni replace the code with the one on drupal 7.12 everything works fine!
Comment #52
pmos commentedi restored only the following code and everything worked fine again
diff --git a/includes/common.inc b/includes/common.inc
index 268e36b..37c3295 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2955,8 +2955,13 @@ function drupal_get_css($css = NULL, $skip_alter = FALSE) {
// Provide the page with information about the individual CSS files used,
// information not otherwise available when CSS aggregation is enabled.
- $setting['ajaxPageState']['css'] = array_fill_keys(array_keys($css), 1);
- $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting);
+ // Skip if no files were added to the page or jQuery.extend() will overwrite
+ // the Drupal.settings.ajaxPageState.css Object with an empty Array.
+ // Cast the Array to an Object to be on the safe side even if not empty.
+ if (!empty($css)) {
+ $setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1);
+ $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting);
+ }
return drupal_render($styles);
}
Comment #53
kristiaanvandeneynde@pmos That code does nothing new except check if the $css variable is empty and, if not, casting the created array to an object to prevent the issues summarized in the original post.
I suggest you open a new ticket in the Hierarchical Select queue and refer to this one in it.
Comment #53.0
kristiaanvandeneyndeUpdated to reflect last patch.