diff --git a/core/modules/file/file.module b/core/modules/file/file.module index a6fc3f6ebe..0da20f2302 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -842,7 +842,11 @@ 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->getTransliteratedFilename(); + + $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) { diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index a68b9bae9b..45f091fc4f 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -2,6 +2,7 @@ namespace Drupal\file\Entity; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityStorageInterface; @@ -274,4 +275,37 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { return $fields; } + /** + * Gets the transliterated filename. + * + * @return string + * The transliterated filename. If the filename transliteration options is + * disabled, the raw filename of the file. + */ + public function getTransliteratedFilename() { + + // If the transliteration option is enabled, transliterate the filename. + if (\Drupal::config('system.file')->get('filename_transliteration')) { + $langcode = $this->languageManager()->getCurrentLanguage()->getId(); + + // Transliterate and sanitize the destination filename. + $filename = \Drupal::transliteration() + ->transliterate($this->getFilename(), $langcode, ''); + + // 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 = Unicode::strtolower($filename); + } + else { + $filename = $this->getFilename(); + } + + return $filename; + } + } diff --git a/core/modules/file/tests/src/Kernel/SaveTest.php b/core/modules/file/tests/src/Kernel/SaveTest.php index 87eb17b20d..2c5dc021f5 100644 --- a/core/modules/file/tests/src/Kernel/SaveTest.php +++ b/core/modules/file/tests/src/Kernel/SaveTest.php @@ -83,4 +83,47 @@ public function testFileSave() { } + /** + * Test file name transliteration. + * + * @dataProvider provideFilenames + */ + public function testFileNameTransliteration($transliterationActive, $originalName, $expectedName) { + // Enable or disable the transliteration. + \Drupal::configFactory() + ->getEditable('system.file') + ->set('filename_transliteration', $transliterationActive) + ->save(); + + // Create a new file entity. + $file = File::create([ + 'uid' => 1, + 'filename' => $originalName, + 'uri' => 'public://llama.txt', + 'filemime' => 'text/plain', + 'status' => FILE_STATUS_PERMANENT, + ]); + + // Check the results of the transliteration. + $this->assertEquals($expectedName, $file->getTransliteratedFilename()); + } + + /** + * Provides data for tests. + * + * @return array + * Provides an array of filenames. + */ + public function provideFilenames() { + return [ + [FALSE, 'llåma.txt', 'llåma.txt'], + [TRUE, 'llåma.txt', 'llama.txt'], + [TRUE, 'll@ma.txt', 'llma.txt'], + [TRUE, 'Llama.txt', 'llama.txt'], + [TRUE, 'll ama.txt', 'll_ama.txt'], + [TRUE, 'll___ama.txt', 'll_ama.txt'], + [TRUE, 'll---ama.txt', 'll-ama.txt'], + ]; + } + } 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 021840c256..2d15553e8f 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -288,6 +288,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 ff0b792433..eac22394e2 100644 --- a/core/modules/system/src/Form/FileSystemForm.php +++ b/core/modules/system/src/Form/FileSystemForm.php @@ -131,6 +131,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#description' => t('Orphaned files are not referenced from any content but remain in the file system and may appear in administrative listings. Warning: If enabled, orphaned 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('Check if transliteration should be enabled for uploaded filenames.'), + ]; + return parent::buildForm($form, $form_state); } @@ -140,7 +147,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', $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/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php index c8e800b61d..3f0cafce50 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 22e9ba7593..310faa1c09 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,