Problem/Motivation
This bug affects all views using bulk operations (content and people). Comments do not have this problem as it's not a view, cf #47. It's critical because if you do not pay attention, you might be deleting the wrong data. What happens is this:
- Select two nodes
- Meanwhile new nodes are been created
- Hit 'Apply to selected items: other (new) items are presented in the confirm form
- Hit confirm: not the selected items are removed, but the new ones
Tested with and without caching on views, as user 1.
This isn't the easiest one to reproduce, but we saw this happening on a very busy site where we basically deleted the wrong nodes or user accounts. I created a small drush script and video in which you see it happening. There's no sound in it, but what you'll see happening is this:
https://www.youtube.com/watch?v=sLfIO71yW84
- Create 5 nodes with prefix drupal
- Select 2 and 3 and hit apply, you'll see the right ones selected
- Go back, select 2 and 3 again, then start a new drush command to create 5 nodes with prefix joomla
- While this script is running, hit 'apply selected items' and it will select 'joomla 2 and 3'
- Hit delete and the wrong ones are effectively deleted
For reference, the drush code:
function swentel_drush_command() {
return array(
'create-new-nodes' => [
'description' => 'create new nodes'
],
);
}
function drush_swentel_create_new_nodes($prefix = 'old', $limit = 5) {
$a = 1;
while ($limit != 0) {
$node = \Drupal\node\Entity\Node::create(['type' => 'page', 'status' => 1, 'title' => $prefix . ' ' . $a]);
$node->save();
sleep(1);
$limit--;
$a++;
}
}
Proposed resolution
Use $form_state->getUserInput()
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #40 | 2846782-40.patch | 3.51 KB | swentel |
| #40 | 2846782-40-fail.patch | 2.68 KB | swentel |
| #40 | 2846782-40-interdiff.txt | 2.27 KB | swentel |
| #39 | 2846782-39-interdiff.txt | 833 bytes | swentel |
| #39 | 2846782-39.patch | 2.63 KB | swentel |
Comments
Comment #2
swentel commentedComment #3
dawehnerWow we should use IDs/uuids or something like that in the form.
Comment #4
gábor hojtsyBUTBUT we use the entity ID, langcode and revision key:
We are NOT using its relative position in the form at all.
Comment #5
tim.plunkettI can reproduce, and I also am as confused as @Gábor Hojtsy.
Looking into this.
Comment #6
tacituseu commentedBut in
BulkForm::viewsForm()the way it is used:"#default_value" is filled in from $form_state using "$row_index", so when new nodes are added, and the "Apply to selected items" is pressed, form is rebuilt and the new nodes "take place" of the old ones, because default sort is by "Updated" column.
Comment #7
damiankloip commentedWhy in gods name are we JSON encoding and base64 encoding the form keys again?!
Comment #8
tim.plunkettThe encoding is from #2599246: data in bulk_form_key should not be separated by -.
Comment #9
damiankloip commentedThanks Tim! It's bringing back memories now.
Comment #10
dawehner@tacituseu Great find!
Comment #11
swentel commentedFailing patch
Comment #13
tacituseu commentedFix combined with test from #11
Comment #14
tacituseu commentedAnd again...
Comment #16
tacituseu commentedWon't be so easy, it goes all the way down to
Drupal\Core\Render\Element\Checkbox::valueCallback():Here,
$inputholds the proper (old) value from$_POSTbut the$elementis for the "newly added" node and has its$bulk_form_key, reason is:the old form and rebuilt one have same id for first checkbox, but the rebuilt one has "newly added" node in there, to fix it
$bulk_form_keywould have to be part of the id, something like:but it won't work since views rely on it being indexed by row number, so maybe something like:
Comment #17
tacituseu commentedEven if proper/unique id would fix the "deleting wrong node" part of the problem, the other part of it is that nodes selected at the very bottom of the page would not get chosen action applied.
Comment #18
swentel commentedThis is a fix, which is totally wrong of course, but just wanted to see if this makes it all green.
Comment #19
swentel commented\Drupal::request()->request->get($this->options['id']) is probably a better alternative .. again, probably not the right fix.
Comment #20
tacituseu commentedThis should work as a data loss fix, doesn't use raw user input, instead plays with '#parents', but still doesn't cover #17.
Comment #22
tacituseu commentedThe failures are from tests assuming,
['node_bulk_form[0]' => TRUE]would check the first checkbox, like in:$this->drupalPostForm(NULL, ['node_bulk_form[0]' => TRUE], 'Apply to selected items');which it no longer does, but the code works.
Comment #23
tacituseu commentedFixing tests by xpath'ing out the proper names/ids for checkboxes.
Comment #25
tacituseu commentedComment #26
swentel commentedInteresting approach, that's indeed better than the way I tried! :)
I wonder whether we could skip the loop over the views result. Even better would be for views to even completely skip the actual query, but I'm not sure if that's possible at all.
Comment #28
tacituseu commented@swentel: this was just to preserve logic that was already there, the magic is in '#parents'.
New patch, with fewer errors, hopefully.
Comment #31
tacituseu commentedComment #33
tacituseu commentedComment #35
tacituseu commentedRandom segfault in
Drupal\node\Tests\Views\FrontPageTest.Comment #36
tacituseu commentedComment #38
hyperlinkedPlease also take a look at an issue I opened (#2845634). It's definitely a related issue, but I'm not sure if it's the exact same issue.
I experienced this same issue in a different way. I found that using AJAX to filter or sort a table was enough to cause the index to lose integrity.
The difference between this report and mine (#2845634) is that in this one, the index loses integrity when new content is added while a user is in the middle of performing a bulk action. In my report, no new content needs to be introduced. If AJAX sorting or filtering is used, the index will lose integrity and the actions will be performed on the wrong nodes.
When I did experience this behavior, the following conditions were true:
The exact same view, will work as expected if AJAX is turned off.
Unfortunately, I'm having difficulty reproducing this on a clean install. I'm mentioning it here in case it helps identify the root cause here.
If anyone thinks it would be valuable for me to try to pin down exactly how to reproduce the issue I described in #2845634, let me know. I'll see if I can identify the exact factors.
Comment #39
swentel commentedRight: but this still won't solve what you described in #17 - which is why I think we should probably skip iterating the views result completely once we have values in in the selection completely (or something esle), but that would make viewsForm() even more complex I think.
Alternatively. MenuForm::submitOverviewForm() uses $form_state->getUserInput() .. which is elegant too (at least I think so).
So, another try, completely different patch.
interdiff is against #18
(note, don't want to necessarily hijack, it's probably good to have two approaches)
Comment #40
swentel commentedAnother patch, now with tests to prove the last selection. Interdiff against #39
With new fail patch and ok patch.
Comment #41
swentel commentedugh, this line can be removed of course, no need to run it twice
Comment #42
gábor hojtsyComment #43
tacituseu commented@hyperlinked (#38): I noticed some AJAX related issues in Chrome related to going back in history, it seems to go back to base non-ajax html and checkboxes get mixed-up due to having the same id as ones updated by AJAX, to replicate:
0. Make sure the latest node is 'Unpublished', and all the other 'Published'
1. Open 'admin/content'
2. Filter out the top node (e.g. filter for 'Published' only)
3. Select first two nodes and click 'Apply to selected items'
4. All is as expected on confirmation page
5. Go back in browser history to previous page
6. Observe the form being reset to the before AJAX state and the 'Unpublished' node being now selected
@swentel (#39): Yes, I consider my patch to be only a band-aid, i.e. it will prevent you from shooting yourself in the wrong foot. If anyone it's me that hijacked, my approach will get me no further anyway.
Maybe using temp storage and rebuilding the form from it (like you mentioned in #26) could help, but I'm not familiar enough with views module inner workings to take it further, especially with AJAX in the mix.
I feel though that making sure each checkbox has unique id/name is essential part of the solution.
I also worked on extending test coverage to also cover deletion and node from the next page getting into selection (took slightly different approach - attached for comparison).
Comment #45
swentel commentedSetting to needs review as #40 is green.
Comment #46
cilefen commented@alexpott, @xjm, @effulgentsia, @catch and I discussed this issue and agreed this is clearly a critical bug because of the demonstrated data loss.
Comment #47
swentel commentedNote, comments don't have this problem as this is not a view. Interestingly enough, CommentDeleteMultiple also uses getUserInput(), so this validates my approach in #40.
Comment #48
swentel commentedComment #49
tacituseu commentedOn its way out though #1986606-326: Convert the comments administration screen to a view (3rd point).
Comment #50
borisson_@swentel and I discussed this issue and the fix at the global sprint weekend, I had to let this simmer for a while to fully understand the reason why #40 is a good way to fix this, but I feel confident that patch is correct.
It's small, self-contained and has sufficient test coverage.
Maybe we could add docs in the
::viewsFormSubmitwhy we're usinggetUserInputbut I don't think that's needed really, the test should be enough to not shoot ourselves in the feet again.@tacituseu: if the comments view is going to change, it will break as well. So we should probably add a test similar to the one added here for nodes for comments as well (in a seperate issue as a follow-up to this one or in the other issue).
That way we know the behavior happening here won't happen for comments as well.
Anyway, I'm going to rtbc #40 based on that.
Comment #51
tacituseu commentedComment #52
swentel commentedComment #53
tacituseu commentedYou could help it to look a lot less scary by reinforcing
BulkForm::loadEntityFromBulkFormKey()a little, something like:would prevent it from trusting database drivers too much :)
Comment #57
xjmOh, #40 is so beautifully simple!
In the future, please always reupload the patch intended for commit. While hiding files helps make the issue summary focus on the right patch, the last patch on the issue will be automatically retested when the issue is RTBC, but other patches won't. Committers can also get confused when reading issue comments from the bottom up as we often do. :) So it's a good way to avoid regressions or mistakes.
@hyperlinked, can you test your issue against the latest HEAD now that this issue is fixed, and post on #2845634: Node Bulk Operation Performs Operations on Wrong Nodes when AJAX Filtering/Sorting is Used whether it also resolves your issue? It did not at first look like it would, but hopefully you can follow up now that this one is fixed.
I do agree with adding inline documentation as @borisson_ suggested. Since this is a data loss critical, I added some inline docs on commit rather than further delay the issue. If my added docs are incorrect or need improvement, please file a followup for fixing them:
Similarly, for exploring more complete solutions to improve this generally (as discussed elsewhere on the issue), let's file a followup for that too. That way we can save some data between now and when we agree on other ideas. :)
Committed to 8.4.x and 8.3.x Thanks for fixing this so quickly!
Comment #58
xjmComment #59
swentel commented@xjm didn't expect that the patch would get in already which is why I waited before re-uploading again :)
we might need a tiny follow up for #41 though, unless you want to fix this as well ?
Comment #60
hyperlinked@xjm, I've tested the 8.3 HEAD against my build and it resolved the issue that I reported in #2845634. It was the same issue.
Comment #61
xjmThanks @hyperlinked!
Oops yeah @swentel, I missed #41. Followup sounds good.
Comment #63
swentel commentedFollow up at #2857846: Remove obsolete get call in ViewsBulkTest
Comment #64
ammar qala commentedI'm using Drupal 8.9.20 and this issue still happens.
Any solution?