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

Comments

charlotte.b created an issue. See original summary.

charlotte.b’s picture

StatusFileSize
new1.28 KB
charlotte.b’s picture

Status: Active » Needs review
cilefen’s picture

We 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.

swentel’s picture

Does 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!

swentel’s picture

Issue tags: +JavaScript

Tagging so javascript maintainers will see this.

charlotte.b’s picture

It 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.

droplet’s picture

wim leers’s picture

Related issues:

Can 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.

charlotte.b’s picture

It'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?

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

vijaycs85’s picture

StatusFileSize
new1.33 KB
new1.33 KB

@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.

vijaycs85’s picture

charlotte.b’s picture

Hi @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.

vijaycs85’s picture

Assigned: charlotte.b » Unassigned
Issue summary: View changes
Issue tags: +Needs tests, +Needs manual testing

Thanks @charlotte.b. Updated issue summary with details and tagged for testing.

vijaycs85’s picture

Status: Needs review » Needs work

Back to needs work to add tests.

manarth’s picture

When 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().

gpotter’s picture

Nevermind

mcdruid’s picture

I 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:

  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['title'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Title'),
      '#description' => $this->t('Title must be at least 5 characters in length.'),
      '#required' => TRUE,
    ];

    $form['exam_document'] = [
      '#type' => 'managed_file',
      '#title' => $this->t('Exam Document'),
      '#required' => FALSE,
      '#upload_location' => 'public://',
      '#multiple' => TRUE,
      '#default_value' => '',
      '#upload_validators' => [
        'file_validate_extensions' => ['doc docx txt pdf'],
      ]
    ];

...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.

mysql> SELECT fid, uuid, filename, uri, status, created FROM file_managed WHERE fid > 31;
+-----+--------------------------------------+------------------+---------------------------+--------+------------+
| fid | uuid                                 | filename         | uri                       | status | created    |
+-----+--------------------------------------+------------------+---------------------------+--------+------------+
|  32 | d3713acc-ad6b-430f-af43-42f25674f36a | test_upload.txt  | public://test_upload.txt  |      0 | 1508422451 |
|  33 | c9c25e7a-9c76-48a1-adee-3de19b855357 | another_test.pdf | public://another_test.pdf |      0 | 1508423711 |
|  34 | c539c80b-09c1-48e9-97e6-c4d74d6710d7 | another_test.pdf | public://another_test.pdf |      0 | 1508423711 |
+-----+--------------------------------------+------------------+---------------------------+--------+------------+

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:

[19/Oct/2017:15:35:11 +0100] "POST /examples/fapi-example/simple-form?element_parents=exam_document&ajax_form=1&_wrapper_format=drupal_ajax&_wrapper_format=drupal_ajax HTTP/1.1" 200 5886
[19/Oct/2017:15:35:11 +0100] "POST /examples/fapi-example/simple-form?element_parents=exam_document&ajax_form=1&_wrapper_format=drupal_ajax&_wrapper_format=drupal_ajax HTTP/1.1" 200 5886

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.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

gbirch’s picture

mcdruid’s picture

StatusFileSize
new1.33 KB

Nope, 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.

mcdruid’s picture

Status: Needs work » Needs review

NR for the testbot

mcdruid’s picture

Status: Needs review » Needs work

Back to Needs Work for tests to be added.

zviryatko’s picture

StatusFileSize
new1.94 KB
new3.45 KB
new2.12 KB
zviryatko’s picture

Status: Needs work » Needs review
Issue tags: +KharkivCS

The last submitted patch, 29: 2884052-29-test-only.patch, failed testing. View results

The last submitted patch, 29: 2884052-29.patch, failed testing. View results

The last submitted patch, 29: 2884052-29-test-only.patch, failed testing. View results

The last submitted patch, 29: 2884052-29-test-only.patch, failed testing. View results

The last submitted patch, 29: 2884052-29-test-only.patch, failed testing. View results

andypost’s picture

13:15:09 Build timed out (after 110 minutes). Marking the build as aborted.

So test only patch hangs somehow https://dispatcher.drupalci.org/job/drupal_patches/56961/console

zviryatko’s picture

StatusFileSize
new3.56 KB
new2.25 KB
new2.06 KB

Status: Needs review » Needs work

The last submitted patch, 37: 2884052-37-test-only.patch, failed testing. View results

andypost’s picture

Status: Needs work » Reviewed & tested by the community

Great! Next time please attach test only patch first to keep issue status NR.

lauriii’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/file/file.js
@@ -92,7 +92,7 @@
+      $(event.target).closest('.js-form-managed-file').find('.managed-file-upload-button').trigger('mousedown');

+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -243,7 +243,12 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
+          'managed-file-upload-button',

We could use a data attribute so it would be easier to remember that there's some JS attached to the element.

GrandmaGlassesRopeMan’s picture

+++ b/core/modules/file/file.js
@@ -92,7 +92,7 @@
+      $(event.target).closest('.js-form-managed-file').find('.managed-file-upload-button').trigger('mousedown');

Please make your changes to the .es6.js file and then use the transpile process.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

drclaw’s picture

Status: Needs work » Needs review
StatusFileSize
new1.54 KB
new2.25 KB
new4 KB

@laurii would a js- prefix work?

@drpal updated es6 file and transpiled

The last submitted patch, 43: 2884052-43-test-only.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 43: 2884052-43.patch, failed testing. View results

Madis’s picture

Status: Needs work » Needs review
StatusFileSize
new2.25 KB
new3.98 KB

Rerolled.

The last submitted patch, 46: 2884052-46-test-only.patch, failed testing. View results

krzysztof domański’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs tests

I use on the site. Tested manually.

lauriii’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -243,7 +243,10 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
+        'data-managed-file-js-upload' => '',

How about using the data-drupal-selector which we've kind of standardized on?

krzysztof domański’s picture

Status: Needs work » Needs review
StatusFileSize
new2.04 KB
new3.28 KB

@lauriii Thank you for the suggestion.

We can use data-drupal-selector to distinguish the buttons. Additional 'data-' attributes are not needed.

In the upload button the data-drupal-selector is equal to edit-$form_id-upload-button.
In the remove button the data-drupal-selector is equal to edit-$form_id-remove-button.

+++ b/core/modules/file/file.js
@@ -92,7 +92,7 @@
       }
     },
     triggerUploadButton: function triggerUploadButton(event) {
-      $(event.target).closest('.js-form-managed-file').find('.js-form-submit').trigger('mousedown');
+      $(event.target).closest('.js-form-managed-file').find('.js-form-submit[data-drupal-selector$="upload-button"]').trigger('mousedown');
     },
     disableFields: function disableFields(event) {
       var $clickedButton = $(this);

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

berdir’s picture

+++ b/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php
@@ -0,0 +1,60 @@
+    $file_system = $this->container->get('file_system');
+    $file_storage = $this->container->get('entity_type.manager')

I 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

krzysztof domański’s picture

StatusFileSize
new976 bytes
new3.25 KB
berdir’s picture

Status: Needs review » Reviewed & tested by the community

Thanks. 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 :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 53: 2884052-53.patch, failed testing. View results

berdir’s picture

Status: Needs work » Reviewed & tested by the community

Random/time related fail:

-'5 years 11 months'
+'6 years'

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php
@@ -0,0 +1,59 @@
+    // Save entire form.
+    $page->find('css', '[value="' . t('Save') . '"]')->click();

This 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...

  /**
   * Test if managed file form element works well with multiple files upload.
   */
  public function testMultipleFilesUpload() {
    $file_system = \Drupal::service('file_system');
    $file_storage = \Drupal::entityTypeManager()->getStorage('file');
    $page = $this->getSession()->getPage();

    $this->drupalGet(Url::fromRoute('file_module_test.managed_test', ['multiple' => TRUE]));

    $paths = [];
    foreach (array_slice($this->drupalGetTestFiles('image'), 0, 2) as $image) {
      $paths[] = $image->filename;
      $page->attachFileToField('files[nested_file][]', $file_system->realpath($image->uri));
      $this->assertSession()->assertWaitOnAjaxRequest();
    }

    // Save entire form.
    $page->pressButton('Save');

    /** @var \Drupal\file\FileInterface[] $files */
    $files = $file_storage->loadByProperties(['filename' => $paths]);
    $this->assertSession()->responseContains((string) t('The file ids are %fids.', ['%fids' => implode(',', array_keys($files))]));
    $this->assertCount(2, $files, 'Only two files expected to be saved.');
  }
krzysztof domański’s picture

Status: Needs work » Needs review
StatusFileSize
new2.55 KB
new2.95 KB

1. Addressed #57.

2. $modules is now a protected property in unit tests
#2822382: Make every $modules property protected on classes extending BrowserTestBase and KernelTestBase

--- a/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php
+++ b/core/modules/file/tests/src/FunctionalJavascript/AjaxFileManagedMultipleTest.php
@@ -18,11 +18,9 @@ class AjaxFileManagedMultipleTest extends WebDriverTestBase {
   }
 
   /**
-   * Modules to enable.
-   *
-   * @var array
+   * {@inheritdoc}
    */
-  public static $modules = ['file_test', 'file', 'file_module_test'];
+  protected static $modules = ['file_test', 'file', 'file_module_test'];
raczernecki’s picture

Status: Needs review » Reviewed & tested by the community
alexpott’s picture

StatusFileSize
new1011 bytes
new1.74 KB
new2.78 KB

I've confirmed the test fails without the fix. Patch attached makes the test a little bit simpler - less t() and easier to read assertions.

The last submitted patch, 60: 2884052-60.test-only.patch, failed testing. View results

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed 982773dea5 to 8.8.x and 20ebfa4348 to 8.7.x. Thanks!

Credited @lauriii and @Berdir for issue review.

  • alexpott committed 982773d on 8.8.x
    Issue #2884052 by zviryatko, Krzysztof Domański, alexpott, drclaw,...
alexpott’s picture

Version: 8.8.x-dev » 8.7.x-dev

  • alexpott committed 20ebfa4 on 8.7.x
    Issue #2884052 by zviryatko, Krzysztof Domański, alexpott, drclaw,...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.