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 8ed377e..7e7a6ca 100644 --- a/core/modules/file/file.es6.js +++ b/core/modules/file/file.es6.js @@ -22,35 +22,22 @@ * Detaches validation for file extensions. */ Drupal.behaviors.fileValidateAutoAttach = { - attach: function (context, settings) { + attach: function (context) { var $context = $(context); var 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: function (context, settings, trigger) { var $context = $(context); - var 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); - } } }; @@ -138,11 +125,17 @@ $('.file-upload-js-error').remove(); // Add client side validation for the input[type=file]. - 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.', { + 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 @@ -150,10 +143,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(); @@ -254,4 +248,34 @@ } }; + /** + * 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 += ''; + } + 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 27e2775..ba69238 100644 --- a/core/modules/file/file.js +++ b/core/modules/file/file.js @@ -10,30 +10,17 @@ 'use strict'; Drupal.behaviors.fileValidateAutoAttach = { - attach: function attach(context, settings) { + attach: function attach(context) { var $context = $(context); var 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: function detach(context, settings, trigger) { var $context = $(context); - var elements; - - 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); } } }; @@ -77,15 +64,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(); @@ -140,4 +134,23 @@ window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550'); } }; + + 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 += ''; + } + error += ' '; + error += Drupal.t('Only files with the following extensions are allowed: %extensions.', { + '%extensions': validExtensions.join(', ') + }); + + return '
' + error + '
'; + }; })(jQuery, Drupal); \ No newline at end of file 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 ddfd8f4..e6cb08f 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 9c105c1..9b2f873 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.'); } /**