diff --git a/core/lib/Drupal/Core/Render/Element/File.php b/core/lib/Drupal/Core/Render/Element/File.php
index 07bd415..bf2310a 100644
--- a/core/lib/Drupal/Core/Render/Element/File.php
+++ b/core/lib/Drupal/Core/Render/Element/File.php
@@ -44,7 +44,7 @@ public function getInfo() {
*/
public static function processFile(&$element, FormStateInterface $form_state, &$complete_form) {
if ($element['#multiple']) {
- $element['#attributes'] = ['multiple' => 'multiple'];
+ $element['#attributes']['multiple'] = 'multiple';
$element['#name'] .= '[]';
}
return $element;
diff --git a/core/modules/file/file.es6.js b/core/modules/file/file.es6.js
index 9ab5dc2..9fafcda 100644
--- a/core/modules/file/file.es6.js
+++ b/core/modules/file/file.es6.js
@@ -19,36 +19,23 @@
* Detaches validation for file extensions.
*/
Drupal.behaviors.fileValidateAutoAttach = {
- attach(context, settings) {
+ attach: function (context) {
const $context = $(context);
let elements;
- function initFileValidation(selector) {
- $context.find(selector)
- .once('fileValidate')
- .on('change.fileValidate', { extensions: elements[selector] }, Drupal.file.validateExtension);
- }
-
- if (settings.file && settings.file.elements) {
- elements = settings.file.elements;
- Object.keys(elements).forEach(initFileValidation);
- }
+ $context.find('[data-drupal-validate-extensions]')
+ .once('fileValidate')
+ .on('change.fileValidate', Drupal.file.validateExtension);
},
detach(context, settings, trigger) {
const $context = $(context);
- let elements;
- function removeFileValidation(selector) {
- $context.find(selector)
+ if (trigger === 'unload') {
+ $context.find('[data-drupal-validate-extensions]')
.removeOnce('fileValidate')
.off('change.fileValidate', Drupal.file.validateExtension);
}
-
- if (trigger === 'unload' && settings.file && settings.file.elements) {
- elements = settings.file.elements;
- Object.keys(elements).forEach(removeFileValidation);
- }
- },
+ }
};
/**
@@ -135,11 +122,17 @@
$('.file-upload-js-error').remove();
// Add client side validation for the input[type=file].
- const extensionPattern = event.data.extensions.replace(/,\s*/g, '|');
- if (extensionPattern.length > 1 && this.value.length > 0) {
- const acceptableMatch = new RegExp(`\\.(${extensionPattern})$`, 'gi');
- if (!acceptableMatch.test(this.value)) {
- const error = Drupal.t('The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.', {
+ var validExtensions = JSON.parse(event.target.getAttribute('data-drupal-validate-extensions'));
+ if (validExtensions.length > 1 && event.target.value.length > 0) {
+ var acceptableMatch = new RegExp('\\.(' + validExtensions.join('|') + ')$', 'i');
+ var testSubjects = event.target.files;
+ if (!testSubjects) {
+ testSubjects = [{name: event.target.value}];
+ }
+ var invalidFiles = [];
+ for (var i = 0; i < testSubjects.length; i++) {
+ var testSubject = testSubjects[i];
+ if (!acceptableMatch.test(testSubject.name)) {
// According to the specifications of HTML5, a file upload control
// should not reveal the real local path to the file that a user
// has selected. Some web browsers implement this restriction by
@@ -147,10 +140,11 @@
// confusion by leaving the user thinking perhaps Drupal could not
// find the file because it messed up the file path. To avoid this
// confusion, therefore, we strip out the bogus fakepath string.
- '%filename': this.value.replace('C:\\fakepath\\', ''),
- '%extensions': extensionPattern.replace(/\|/g, ', '),
- });
- $(this).closest('div.js-form-managed-file').prepend(`
${error}
`);
+ invalidFiles.push(testSubject.name.replace('C:\\fakepath\\', ''));
+ }
+ }
+ if (invalidFiles.length) {
+ $(this).closest('div.js-form-managed-file').prepend(Drupal.theme('fileValidationError', invalidFiles, validExtensions));
this.value = '';
// Cancel all other change event handlers.
event.stopImmediatePropagation();
@@ -250,4 +244,35 @@
window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550');
},
};
+
+ /**
+ * Error message of file validation.
+ *
+ * @param {Array.} invalidFiles
+ * Array of invalid filenames.
+ * @param {string} validExtensions
+ * List of accepted files extensions.
+ *
+ * @return {string}
+ * Error message.
+ */
+ Drupal.theme.fileValidationError = function (invalidFiles, validExtensions) {
+ var error = Drupal.formatPlural(invalidFiles.length, 'The selected file %filename cannot be uploaded.', '@count of the selected files cannot be uploaded.', {
+ '%filename': invalidFiles[0]
+ });
+ if (invalidFiles.length > 1) {
+ error += '';
+ invalidFiles.forEach(function (file) {
+ error += '- ' + Drupal.formatString('%filename', {'%filename': file}) + '
';
+ });
+ error += '
';
+ }
+ error += ' ';
+ error += Drupal.t('Only files with the following extensions are allowed: %extensions.', {
+ '%extensions': validExtensions.join(', ')
+ });
+
+ return '' + error + '
';
+ };
+
}(jQuery, Drupal));
diff --git a/core/modules/file/file.js b/core/modules/file/file.js
index 4d51bb0..81490a1 100644
--- a/core/modules/file/file.js
+++ b/core/modules/file/file.js
@@ -7,30 +7,17 @@
(function ($, Drupal) {
Drupal.behaviors.fileValidateAutoAttach = {
- attach: function attach(context, settings) {
+ attach: function attach(context) {
var $context = $(context);
var elements = void 0;
- function initFileValidation(selector) {
- $context.find(selector).once('fileValidate').on('change.fileValidate', { extensions: elements[selector] }, Drupal.file.validateExtension);
- }
-
- if (settings.file && settings.file.elements) {
- elements = settings.file.elements;
- Object.keys(elements).forEach(initFileValidation);
- }
+ $context.find('[data-drupal-validate-extensions]').once('fileValidate').on('change.fileValidate', Drupal.file.validateExtension);
},
detach: function detach(context, settings, trigger) {
var $context = $(context);
- var elements = void 0;
-
- function removeFileValidation(selector) {
- $context.find(selector).removeOnce('fileValidate').off('change.fileValidate', Drupal.file.validateExtension);
- }
- if (trigger === 'unload' && settings.file && settings.file.elements) {
- elements = settings.file.elements;
- Object.keys(elements).forEach(removeFileValidation);
+ if (trigger === 'unload') {
+ $context.find('[data-drupal-validate-extensions]').removeOnce('fileValidate').off('change.fileValidate', Drupal.file.validateExtension);
}
}
};
@@ -74,15 +61,22 @@
$('.file-upload-js-error').remove();
- var extensionPattern = event.data.extensions.replace(/,\s*/g, '|');
- if (extensionPattern.length > 1 && this.value.length > 0) {
- var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi');
- if (!acceptableMatch.test(this.value)) {
- var error = Drupal.t('The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.', {
- '%filename': this.value.replace('C:\\fakepath\\', ''),
- '%extensions': extensionPattern.replace(/\|/g, ', ')
- });
- $(this).closest('div.js-form-managed-file').prepend('' + error + '
');
+ var validExtensions = JSON.parse(event.target.getAttribute('data-drupal-validate-extensions'));
+ if (validExtensions.length > 1 && event.target.value.length > 0) {
+ var acceptableMatch = new RegExp('\\.(' + validExtensions.join('|') + ')$', 'i');
+ var testSubjects = event.target.files;
+ if (!testSubjects) {
+ testSubjects = [{ name: event.target.value }];
+ }
+ var invalidFiles = [];
+ for (var i = 0; i < testSubjects.length; i++) {
+ var testSubject = testSubjects[i];
+ if (!acceptableMatch.test(testSubject.name)) {
+ invalidFiles.push(testSubject.name.replace('C:\\fakepath\\', ''));
+ }
+ }
+ if (invalidFiles.length) {
+ $(this).closest('div.js-form-managed-file').prepend(Drupal.theme('fileValidationError', invalidFiles, validExtensions));
this.value = '';
event.stopImmediatePropagation();
@@ -133,4 +127,23 @@
window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550');
}
};
-})(jQuery, Drupal);
\ No newline at end of file
+
+ Drupal.theme.fileValidationError = function (invalidFiles, validExtensions) {
+ var error = Drupal.formatPlural(invalidFiles.length, 'The selected file %filename cannot be uploaded.', '@count of the selected files cannot be uploaded.', {
+ '%filename': invalidFiles[0]
+ });
+ if (invalidFiles.length > 1) {
+ error += '';
+ invalidFiles.forEach(function (file) {
+ error += '- ' + Drupal.formatString('%filename', { '%filename': file }) + '
';
+ });
+ error += '
';
+ }
+ error += ' ';
+ error += Drupal.t('Only files with the following extensions are allowed: %extensions.', {
+ '%extensions': validExtensions.join(', ')
+ });
+
+ return '' + error + '
';
+ };
+})(jQuery, Drupal);
diff --git a/core/modules/file/src/Element/ManagedFile.php b/core/modules/file/src/Element/ManagedFile.php
index 45c8aa7..1339438 100644
--- a/core/modules/file/src/Element/ManagedFile.php
+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -2,6 +2,7 @@
namespace Drupal\file\Element;
+use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
@@ -342,10 +343,9 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
}
}
- // Add the extension list to the page as JavaScript settings.
if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
- $extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0])));
- $element['upload']['#attached']['drupalSettings']['file']['elements']['#' . $element['#id']] = $extension_list;
+ $extension_list = array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0]));
+ $element['upload']['#attributes']['data-drupal-validate-extensions'] = Json::encode($extension_list);
}
// Let #id point to the file element, so the field label's 'for' corresponds
diff --git a/core/modules/file/src/Tests/FileFieldWidgetTest.php b/core/modules/file/src/Tests/FileFieldWidgetTest.php
index ad47d77..ff0ef94 100644
--- a/core/modules/file/src/Tests/FileFieldWidgetTest.php
+++ b/core/modules/file/src/Tests/FileFieldWidgetTest.php
@@ -428,6 +428,9 @@ public function testWidgetValidation() {
$test_file_image = $this->getTestFile('image');
$name = 'files[' . $field_name . '_0]';
+ // Check field is marked with expected attr for client-side validation.
+ $this->assertFieldByXPath('//input[@type="file" and @data-drupal-validate-extensions=\'["txt"]\']', NULL, 'data-drupal-validate-extensions is present with expected value.');
+
// Upload file with incorrect extension, check for validation error.
$edit[$name] = drupal_realpath($test_file_image->getFileUri());
switch ($type) {
diff --git a/core/modules/file/src/Tests/FileManagedFileElementTest.php b/core/modules/file/src/Tests/FileManagedFileElementTest.php
index 8abff25..b102e47 100644
--- a/core/modules/file/src/Tests/FileManagedFileElementTest.php
+++ b/core/modules/file/src/Tests/FileManagedFileElementTest.php
@@ -18,6 +18,10 @@ public function testManagedFile() {
$this->drupalGet('file/test');
$this->assertFieldByXpath('//input[@name="files[nested_file]" and @size="13"]', NULL, 'The custom #size attribute is passed to the child upload element.');
+ // Check that relevant data- attributes are present for validations
+ $this->drupalGet('file/test/validation/extension');
+ $this->assertFieldByXPath('//input[@name="files[file]" and @data-drupal-validate-extensions=\'["txt"]\']', NULL, 'The data-drupal-validate-extensions attribute is present with the expected value on the upload element.');
+
// Perform the tests with all permutations of $form['#tree'],
// $element['#extended'], and $element['#multiple'].
$test_file = $this->getTestFile('text');
diff --git a/core/modules/file/tests/file_module_test/file_module_test.routing.yml b/core/modules/file/tests/file_module_test/file_module_test.routing.yml
index 15f7d0f..0c85f6e 100644
--- a/core/modules/file/tests/file_module_test/file_module_test.routing.yml
+++ b/core/modules/file/tests/file_module_test/file_module_test.routing.yml
@@ -8,3 +8,11 @@ file_module_test.managed_test:
default_fids: NULL
requirements:
_access: 'TRUE'
+
+file_module_test.managed_validations_test:
+ path: '/file/test/validation/{validation_type}'
+ defaults:
+ _form: '\Drupal\file_module_test\Form\FileModuleTestValidationForm'
+ validation_type: 'extension'
+ requirements:
+ _access: 'TRUE'
diff --git a/core/modules/file/tests/file_module_test/src/Form/FileModuleTestValidationForm.php b/core/modules/file/tests/file_module_test/src/Form/FileModuleTestValidationForm.php
new file mode 100644
index 0000000..e6a0306
--- /dev/null
+++ b/core/modules/file/tests/file_module_test/src/Form/FileModuleTestValidationForm.php
@@ -0,0 +1,70 @@
+ ['txt']];
+ break;
+ case 'size':
+ $validator = ['file_validate_size' => [1024]];
+ break;
+ default:
+ return FALSE;
+ }
+
+ $form['file'] = [
+ '#type' => 'managed_file',
+ '#title' => $this->t('Managed File'),
+ '#upload_location' => 'public://test',
+ '#progress_message' => $this->t('Please wait...'),
+ '#upload_validators' => $validator,
+ ];
+
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ drupal_set_message($this->t('Test submission handler was allowed to execute.'));
+ }
+
+}
diff --git a/core/modules/file/tests/src/FunctionalJavascript/ClientValidationTest.php b/core/modules/file/tests/src/FunctionalJavascript/ClientValidationTest.php
new file mode 100644
index 0000000..edc0633
--- /dev/null
+++ b/core/modules/file/tests/src/FunctionalJavascript/ClientValidationTest.php
@@ -0,0 +1,94 @@
+drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
+
+ // Create a field with a basic filetype restriction.
+ $field_name = strtolower($this->randomMachineName());
+ $field_settings = [
+ 'file_extensions' => 'png',
+ ];
+ $formatter_settings = [
+ 'image_style' => 'large',
+ 'image_link' => '',
+ ];
+ $this->createImageField($field_name, 'article', [], $field_settings, [], $formatter_settings);
+
+ // Log in as a content author who can create Articles.
+ $user = $this->drupalCreateUser([
+ 'access content',
+ 'create article content',
+ 'edit any article content',
+ 'delete any article content',
+ ]);
+ $this->drupalLogin($user);
+
+ $uri = 'public://file.txt';
+ file_unmanaged_save_data('Drupal rocks!', $uri, FILE_EXISTS_REPLACE);
+ $this->file_path = \Drupal::service('file_system')->realpath($uri);
+ }
+
+ /**
+ * Tests the client side validation on file fields.
+ */
+ public function testDisallowedExtensionErrorMessage() {
+ $this->drupalGet('node/add/article');
+ $assert = $this->assertSession();
+
+ // Test uploading a file with a file extension not allowed.
+ // Try to upload a file with an unallowed extension
+ $input_selector = 'input.form-file';
+ $input = $assert->elementExists('css', $input_selector);
+ $input->attachFile($this->file_path);
+
+ // Trigger the upload logic with a mock "drop" event.
+ $script = 'var e = jQuery.Event("drop");'
+ . 'e.originalEvent = {dataTransfer: {files: jQuery("input.form-file").get(0).files}};'
+ . 'e.preventDefault = e.stopPropagation = function () {};'
+ . 'jQuery("input.form-file").trigger(e);';
+ $this->getSession()->executeScript($script);
+
+ // Verify that the error message is there.
+ $error_selector = '.file-upload-js-error';
+ // Verify that there is one and only one error message on the page.
+ $assert->waitForElement('css', $error_selector);
+ $condition = "jQuery('$error_selector').length == 1";
+ $this->assertJsCondition($condition);
+ $assert->elementExists('css', $error_selector);
+ }
+
+}
diff --git a/core/modules/image/src/Tests/ImageFieldDisplayTest.php b/core/modules/image/src/Tests/ImageFieldDisplayTest.php
index 8061526..3cd3a3e 100644
--- a/core/modules/image/src/Tests/ImageFieldDisplayTest.php
+++ b/core/modules/image/src/Tests/ImageFieldDisplayTest.php
@@ -325,8 +325,13 @@ public function testImageFieldSettings() {
'files[' . $field_name . '_2][]' => drupal_realpath($test_image->uri),
];
$this->drupalPostAjaxForm(NULL, $edit, $field_name . '_2_upload_button');
- $this->assertNoRaw('');
- $this->assertRaw('');
+ $this->assertNoFieldByXPath('//input[@id="edit-' . strtr($field_name, '_', '-') . '-2-upload"]', 'Upload field for second file not present after it is uploaded.');
+
+ $this->assertTrue($this->cssSelect('input#edit-' . strtr($field_name, '_', '-') . '-3-upload[multiple=multiple]'), 'Third file field presented having multiple attribute');
+ $this->assertFieldByName('files[' . $field_name . '_3][]', NULL, 'Third file field has correct name');
+ $this->assertTrue($this->cssSelect('input[id=edit-' . strtr($field_name, '_', '-') . '-3-upload][size=22]'), 'Third file field presented having correct size attribute');
+ $this->assertTrue($this->cssSelect('input.js-form-file.form-file[id=edit-' . strtr($field_name, '_', '-') . '-3-upload]'), 'Third file field presented having correct css classes');
+ $this->assertFieldByXPath('//input[@id="edit-' . strtr($field_name, '_', '-') . '-3-upload"] and @data-drupal-validate-extensions=\'["txt"]\']', NULL, 'The data-drupal-validate-extensions attribute is present with the expected value on the upload element.');
}
/**