Problem/Motivation
When a managed file form element is added to a form with multiple files allowed, once files have been added, it will display a remove selected button. The issue is that this remove button is also a submit button and the javascript triggers all elements with the .js-form-submit class. This means then when you upload another file, it triggers both the upload button and the remove button, resulting in two temporary file entities being created with the same URI. When you hit save, only one of these is marked as permanent, meaning that once the cron job runs, it deletes the temporary file, which results in the permanent filename being deleted from the system.
Steps to reproduce
1. Create a custom form element with multiple set to TRUE:
$form['exam_document'] = array(
'#type' => 'managed_file',
'#title' => t('Exam Document'),
'#required' => FALSE,
'#upload_location' => 'private://',
'#multiple' => TRUE,
'#default_value' => explode(',' , $config->get('exam_document')),
'#upload_validators' => array(
'file_validate_extensions' => array('doc docx txt pdf'),
)
);
2. Add a file to start with, wait for it to upload so that it's shown in the list below the upload element
3. Add another.
4. It'll create duplicate file records for the newer files, one marked as permanent and one as temporary.
Proposed resolution
The code needs updating to target specifically the upload button.
Remaining tasks
- Test (both manual and automated)
User interface changes
No
API changes
No
Data model changes
No
| Comment | File | Size | Author |
|---|---|---|---|
| #60 | 2884052-60.patch | 2.78 KB | alexpott |
| #60 | 2884052-60.test-only.patch | 1.74 KB | alexpott |
| #60 | 58-60-interdiff.txt | 1011 bytes | alexpott |
| #58 | 2884052-57.patch | 2.95 KB | krzysztof domański |
| #58 | interdiff-53-57.txt | 2.55 KB | krzysztof domański |
Comments
Comment #2
charlotte.b commentedComment #3
charlotte.b commentedComment #4
cilefen commentedComment #5
cilefen commentedWe define the loss of stored data as critical. There are a handful of issues related to file deletion that I've related to this issue.
Comment #6
swentel commentedDoes it really need to be a custom form ? It would explain a couple of issues in the queue already with sudden (sometimes random) deletion of files, very nice find!
Comment #7
swentel commentedTagging so javascript maintainers will see this.
Comment #8
charlotte.b commentedIt looks as though the bug won't occur on entity fields as the file widget displays the files in a table above the managed file form element, so it shouldn't trigger any of the remove buttons.
Comment #9
droplet commentedComment #10
wim leersCan the reporters of this issue also look into #2869855: Race condition in file_save_upload causes data loss, which I suspect focused on the wrong thing to point to as the root cause? I think the root cause of that issue (and their steps to reproduce) is the same as the root cause reported here.
Comment #11
charlotte.b commentedIt's definitely a related issue. I've managed to replicate it by hitting save on a custom entity type before the ajax upload call has completed. As this only happens due to the AJAX call, is it worth disabling form submit buttons using JavaScript until we receive a response?
Comment #16
vijaycs85@charlotte.b Thanks for raising the issue. I tried to reproduce the issue with article (updating file field to accept unlimited images), but couldn't reproduce. Could you update steps to reproduce on issue summary please?
Had to reroll as current patch doesn't apply.
Comment #17
vijaycs85Comment #18
charlotte.b commentedHi @vijaycs85 thank you for looking at this.
You'll need to create a custom form with an upload field, rather than an entity display form.
For example, the issue in our case was where we had a custom form in the admin area, with multiple set to TRUE:
$form['exam_document'] = array(
'#type' => 'managed_file',
'#title' => t('Exam Document'),
'#required' => FALSE,
'#upload_location' => 'private://',
'#multiple' => TRUE,
'#default_value' => explode(',' , $config->get('exam_document')),
'#upload_validators' => array(
'file_validate_extensions' => array('doc docx txt pdf'),
)
);
If you add a file to start with, wait for it to upload so that it's shown in the list below the upload element, then add another. It'll create duplicate file records for the newer files, one marked as permanent and one as temporary.
Comment #19
vijaycs85Thanks @charlotte.b. Updated issue summary with details and tagged for testing.
Comment #20
vijaycs85Back to needs work to add tests.
Comment #21
manarth commentedWhen it comes to a reproducible test-case, #2869855: Race condition in file_save_upload causes data loss may help here - it's consistently reproducible by introducing a delay in
hook_file_validate().Comment #22
gpotter commentedNevermind
Comment #23
mcdruid commentedI tested this using the SimpleForm from the examples module's fapi_example module. Adding a managed_file element along the lines of charlotte.b's example:
...puts the upload widget into the form at /examples/fapi-example/simple-form
I was then able to reproduce the issue of getting duplicate entries in the file_managed table when uploading a second file to the field.
I assume you'd need a submit handler etc.. in order for one of the rows to be marked as FILE_STATUS_PERMANENT, but I think having two rows with the same uri (created at the same time) reproduces the problem anyway.
The duplicates were caused by the browser sending two POSTs more-or-less simultaneously:
Applying the patch from #13 (and rebuilding caches to ensure I was getting the updated JS) resolved the issue; no more duplicate POSTs and rows in the file_managed table when adding multiple files to the upload field.
So this fix looks good to me; agree tests would be good, but perhaps made slightly tricky by the fact that the faulty behaviour is client-side.
I am going to un-mark #2869855: Race condition in file_save_upload causes data loss as a duplicate of this; they are very similar in terms of the symptoms / outcomes, but I think the cause is quite different. I'd argue this issue should remain focused on the JS fix in the file module. I'll update the other issue with some findings around the race condition.
Comment #25
gbirch commentedIs this the same issue as https://www.drupal.org/project/drupal/issues/2666746 ?
Comment #26
mcdruid commentedNope, confirmed that it's not the same issue; I can still reproduce the problem as described in #23 with the latest changes committed over in #2666746: Fix simultaneous file uploads re-posting data.
I've also confirmed that the (attached, rerolled) patch still resolves the issue of double-uploads. (If anyone else is testing, ensure you rebuild caches so that you actually get the updated JS.)
This still needs tests though.
Comment #27
mcdruid commentedNR for the testbot
Comment #28
mcdruid commentedBack to Needs Work for tests to be added.
Comment #29
zviryatko commentedComment #30
zviryatko commentedComment #36
andypostSo test only patch hangs somehow https://dispatcher.drupalci.org/job/drupal_patches/56961/console
Comment #37
zviryatko commentedComment #39
andypostGreat! Next time please attach test only patch first to keep issue status NR.
Comment #40
lauriiiWe could use a data attribute so it would be easier to remember that there's some JS attached to the element.
Comment #41
GrandmaGlassesRopeManPlease make your changes to the .es6.js file and then use the transpile process.
Comment #43
drclaw commented@laurii would a js- prefix work?
@drpal updated es6 file and transpiled
Comment #46
Madis commentedRerolled.
Comment #48
krzysztof domańskiI use on the site. Tested manually.
Comment #49
lauriiiHow about using the
data-drupal-selectorwhich we've kind of standardized on?Comment #50
krzysztof domański@lauriii Thank you for the suggestion.
We can use
data-drupal-selectorto distinguish the buttons. Additional 'data-' attributes are not needed.In the upload button the
data-drupal-selectoris equal toedit-$form_id-upload-button.In the remove button the
data-drupal-selectoris equal toedit-$form_id-remove-button.Comment #52
berdirI prefer \Drupal::service() or in the second case \Drupal::entityTypeManager() in tests, see #2066993: Use magic methods to sync container property to \Drupal::getContainer in functional tests
Comment #53
krzysztof domańskiComment #54
berdirThanks. I don't know too much about JS, but this was RTBC before and I think the feedback from @laurii has been incorporated, so back to him it goes :)
Comment #56
berdirRandom/time related fail:
-'5 years 11 months'
+'6 years'
Comment #57
alexpottThis can be a bit simpler - i.e.
$page->pressButton('Save');In fact looking at the test there's a few ways it can be made a little easier to understand - i.e. not having to work out what $path, $paths and $realpath all are...
Comment #58
krzysztof domański1. Addressed #57.
2. $modules is now a protected property in unit tests
#2822382: Make every $modules property protected on classes extending BrowserTestBase and KernelTestBase
Comment #59
raczernecki commentedComment #60
alexpottI've confirmed the test fails without the fix. Patch attached makes the test a little bit simpler - less t() and easier to read assertions.
Comment #62
alexpottCommitted and pushed 982773dea5 to 8.8.x and 20ebfa4348 to 8.7.x. Thanks!
Credited @lauriii and @Berdir for issue review.
Comment #64
alexpott