diff --git a/core/modules/file/file.module b/core/modules/file/file.module index c0003b350a..3831dd151f 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -959,7 +959,8 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL // rename filename.php.foo and filename.php to filename.php.foo.txt and // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads' // evaluates to TRUE. - if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) { + $system_file_config = \Drupal::config('system.file'); + if (!$system_file_config->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) { $file->setMimeType('text/plain'); // The destination filename will also later be used to create the URI. $file->setFilename($file->getFilename() . '.txt'); @@ -989,7 +990,24 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL if (substr($destination, -1) != '/') { $destination .= '/'; } - $file->destination = file_destination($destination . $file->getFilename(), $replace); + + $filename = $file->getFilename(); + // If the transliteration option is enabled, transliterate the filename. + if ($system_file_config->get('filename_transliteration')) { + // Transliterate and sanitize the destination filename. + $filename = \Drupal::transliteration() + ->transliterate($filename, $file->language()->getId(), ''); + // Replace whitespace. + $filename = str_replace(' ', '-', $filename); + // Remove remaining unsafe characters. + $filename = preg_replace('![^0-9A-Za-z_.-]!', '', $filename); + // Remove multiple consecutive non-alphabetical characters. + $filename = preg_replace('/(_)_+|(\.)\.+|(-)-+/', '\\1\\2\\3', $filename); + // Force lowercase to prevent issues on case-insensitive file systems. + $filename = mb_strtolower($filename); + } + $file->destination = file_destination($destination . $filename, $replace); + // If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and // there's an existing file so we need to bail. if ($file->destination === FALSE) { @@ -1031,6 +1049,10 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL continue; } + // Update the filename with any changes as a result of transliteration or + // renaming due to an existing file. + $file->setFilename(\Drupal::service('file_system')->basename($file->destination)); + // Set the permissions on the new file. drupal_chmod($file->getFileUri()); diff --git a/core/modules/file/src/Tests/FileFieldWidgetTest.php b/core/modules/file/src/Tests/FileFieldWidgetTest.php index 3102bd744d..e1fa4e4b2c 100644 --- a/core/modules/file/src/Tests/FileFieldWidgetTest.php +++ b/core/modules/file/src/Tests/FileFieldWidgetTest.php @@ -259,7 +259,7 @@ public function testMultiValuedWidget() { '%field' => $field_name, '@max' => $cardinality, '@count' => count($upload_files_node_creation) + count($upload_files_node_revision), - '%list' => implode(', ', array_fill(0, 3, $test_file->getFilename())), + '%list' => implode(', ', ['text-0_2.txt', 'text-0_3.txt', 'text-0_4.txt']), ]; $this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args)); $node_storage->resetCache([$nid]); @@ -291,7 +291,7 @@ public function testMultiValuedWidget() { '%field' => $field_name, '@max' => $cardinality, '@count' => count($upload_files), - '%list' => $test_file->getFileName(), + '%list' => 'text-0_12.txt', ]; $this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args)); } diff --git a/core/modules/file/tests/src/Functional/SaveUploadTest.php b/core/modules/file/tests/src/Functional/SaveUploadTest.php index bebf47e076..90b4b4578e 100644 --- a/core/modules/file/tests/src/Functional/SaveUploadTest.php +++ b/core/modules/file/tests/src/Functional/SaveUploadTest.php @@ -47,10 +47,17 @@ class SaveUploadTest extends FileManagedTestBase { */ protected $imageExtension; + /** + * The user used by the test. + * + * @var \Drupal\user\Entity\User + */ + protected $account; + protected function setUp() { parent::setUp(); - $account = $this->drupalCreateUser(['access site reports']); - $this->drupalLogin($account); + $this->account = $this->drupalCreateUser(['access site reports']); + $this->drupalLogin($this->account); $image_files = $this->drupalGetTestFiles('image'); $this->image = File::create((array) current($image_files)); @@ -127,6 +134,48 @@ public function testNormal() { $this->assertTrue(is_file('temporary://' . $dir . '/' . trim(drupal_basename($image3_realpath)))); } + /** + * Tests filename transliteration. + */ + public function testTransliteration() { + $file = $this->generateFile('TEXT-œ', 64, 5, 'text'); + + // Upload a file with a name with uppercase and unicode characters. + $edit = [ + 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($file), + 'extensions' => 'txt', + 'is_image_file' => FALSE, + ]; + $this->drupalPostForm('file-test/upload', $edit, t('Submit')); + $this->assertSession()->statusCodeEquals(200); + // Test that the file name has not been transliterated. + $this->assertSession()->responseContains('File name is TEXT-œ.txt.'); + + // Enable transliteration via the UI. + $this->drupalLogin($this->rootUser); + $this->drupalPostForm('admin/config/media/file-system', ['filename_transliteration' => TRUE], 'Save configuration'); + $this->drupalLogin($this->account); + + // Upload a file with a name with uppercase and unicode characters. + $this->drupalPostForm('file-test/upload', $edit, t('Submit')); + $this->assertSession()->statusCodeEquals(200); + // Test that the file name has been transliterated. + $this->assertSession()->responseContains('File name is text-oe.txt.'); + + // Generate another file with a name that will be changed when + // transliteration is on. + $file = $this->generateFile('S Pace--🙈', 64, 5, 'text'); + $edit = [ + 'files[file_test_upload]' => \Drupal::service('file_system')->realpath($file), + 'extensions' => 'txt', + 'is_image_file' => FALSE, + ]; + $this->drupalPostForm('file-test/upload', $edit, t('Submit')); + $this->assertSession()->statusCodeEquals(200); + // Test that the file name has been transliterated. + $this->assertSession()->responseContains('File name is s-pace-.txt.'); + } + /** * Test extension handling. */ diff --git a/core/modules/system/config/install/system.file.yml b/core/modules/system/config/install/system.file.yml index ec8c0533f6..73b6fd7d5c 100644 --- a/core/modules/system/config/install/system.file.yml +++ b/core/modules/system/config/install/system.file.yml @@ -3,3 +3,4 @@ default_scheme: 'public' path: temporary: '' temporary_maximum_age: 21600 +filename_transliteration: false diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index a6f61b68ed..2ac9016656 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -281,6 +281,9 @@ system.file: temporary_maximum_age: type: integer label: 'Maximum age for temporary files' + filename_transliteration: + type: boolean + label: 'Transliterate names of uploaded files' system.image: type: config_object diff --git a/core/modules/system/src/Form/FileSystemForm.php b/core/modules/system/src/Form/FileSystemForm.php index b0ca6d129e..84927c6fd4 100644 --- a/core/modules/system/src/Form/FileSystemForm.php +++ b/core/modules/system/src/Form/FileSystemForm.php @@ -133,6 +133,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#description' => t('Temporary files are not referenced, but are in the file system and therefore may show up in administrative lists. Warning: If enabled, temporary files will be permanently deleted and may not be recoverable.'), ]; + $form['filename_transliteration'] = [ + '#type' => 'checkbox', + '#title' => t('Enable filename transliteration'), + '#default_value' => $config->get('filename_transliteration'), + '#description' => t('Transliteration ensures that filenames do not contain unicode characters.'), + ]; + return parent::buildForm($form, $form_state); } @@ -142,7 +149,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { $config = $this->config('system.file') ->set('path.temporary', $form_state->getValue('file_temporary_path')) - ->set('temporary_maximum_age', $form_state->getValue('temporary_maximum_age')); + ->set('temporary_maximum_age', $form_state->getValue('temporary_maximum_age')) + ->set('filename_transliteration', (bool) $form_state->getValue('filename_transliteration')); if ($form_state->hasValue('file_default_scheme')) { $config->set('default_scheme', $form_state->getValue('file_default_scheme')); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 6eb8c12005..e2687cd6af 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -2173,3 +2173,12 @@ function system_update_8501() { } } } + +/** + * Set filename_transliteration config to the default value. + */ +function system_update_8701() { + \Drupal::configFactory()->getEditable('system.file') + ->set('filename_transliteration', FALSE) + ->save(); +} diff --git a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php index 33c963de3c..dcf11f26ee 100644 --- a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php +++ b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php @@ -52,6 +52,8 @@ class MigrateSystemConfigurationTest extends MigrateDrupal6TestBase { ], // temporary_maximum_age is not handled by the migration. 'temporary_maximum_age' => 21600, + // filename_transliteration is not handled by migration. + 'filename_transliteration' => FALSE, ], 'system.image.gd' => [ 'jpeg_quality' => 75, diff --git a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php index 0216e2dd31..c7968bf844 100644 --- a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php +++ b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php @@ -50,6 +50,8 @@ class MigrateSystemConfigurationTest extends MigrateDrupal7TestBase { ], // temporary_maximum_age is not handled by the migration. 'temporary_maximum_age' => 21600, + // filename_transliteration is not handled by migration. + 'filename_transliteration' => FALSE, ], 'system.image.gd' => [ 'jpeg_quality' => 80,