Generated with the help of an LLM.

Problem/Motivation

Since Drupal 10.2 (#2492171), site builders can enable filename sanitization on /admin/config/media/file-system, including the option Convert to lowercase. According to the change record (https://www.drupal.org/node/2972665), the lowercase option exists "to prevent issues on case-insensitive file systems".

However, \Drupal\file\EventSubscriber\FileEventSubscriber::sanitizeFilename() splits off the extension before sanitizing and re-appends it unchanged:

$extension = pathinfo($filename, PATHINFO_EXTENSION);
if ($extension !== '') {
  $extension = '.' . $extension;
  $filename = pathinfo($filename, PATHINFO_FILENAME);
}
// ...
if ($fileSettings->get('filename_sanitization.lowercase')) {
  // Force lowercase to prevent issues on case-insensitive file systems.
  $filename = mb_strtolower($filename);
}
$event->setFilename($filename . $extension);

As a result, uploading My Photo.JPG results in my-photo.JPG instead of my-photo.jpg. This is unexpected for site builders: the setting says "Convert to lowercase", but the resulting filename still contains uppercase characters. It also undermines the purpose of the setting, because photo.JPG and photo.jpg are exactly the kind of names that collide or cause confusion across case-sensitive and case-insensitive file systems (e.g. local development on macOS vs. production on Linux, or S3-compatible storage).

Keeping the extension out of the other sanitization steps (transliteration, replacing non-alphanumeric characters, deduplicating separators) makes sense, since those could alter or break the extension. Changing only the case of the extension carries no such risk: extension validation and the security munging in SecurityFileUploadEventSubscriber already compare extensions case-insensitively.

Steps to reproduce

  1. Go to /admin/config/media/file-system and enable Convert to lowercase under "Sanitize filenames".
  2. Upload a file named Example File.PDF (or IMG_1234.JPG) through a file or image field.
  3. Observe that the stored filename is example file.PDF (or img_1234.JPG): the base name is lowercased, the extension is not.

Proposed resolution

Lowercase the file extension as well when Convert to lowercase is enabled. Introduce the change gradually, so existing sites don't change behaviour in a minor release:

1. Add an opt-in setting (off by default)

Add a new $settings key, file_sanitization_lowercase_extension, that defaults to FALSE. When it is TRUE and filename_sanitization.lowercase is enabled, FileEventSubscriber::sanitizeFilename() also lowercases the extension:

if ($fileSettings->get('filename_sanitization.lowercase')) {
  // Force lowercase to prevent issues on case-insensitive file systems.
  $filename = mb_strtolower($filename);
  // @todo Remove the setting check and always lowercase the extension in
  //   Drupal 13. See https://www.drupal.org/node/[change-record].
  if (Settings::get('file_sanitization_lowercase_extension', FALSE)) {
    $extension = mb_strtolower($extension);
  }
}

Document the setting in default.settings.php:

/**
 * Convert file extensions to lowercase during filename sanitization.
 *
 * When "Convert to lowercase" is enabled under "Sanitize filenames" on
 * /admin/config/media/file-system, only the part of the filename before the
 * extension is converted to lowercase by default. Set this to TRUE to also
 * convert the file extension to lowercase, e.g. "Photo.JPG" becomes
 * "photo.jpg" instead of "photo.JPG".
 *
 * This will become the default (and only) behavior in Drupal 13.
 *
 * @see https://www.drupal.org/node/[change-record]
 */
# $settings['file_sanitization_lowercase_extension'] = TRUE;

2. Show a warning on the status report

Add a runtime requirement to the File module. It shows a warning on /admin/reports/status when filename_sanitization.lowercase is enabled and file_sanitization_lowercase_extension is not set to TRUE:

#[Hook('runtime_requirements')]
public function runtimeRequirements(): array {
  $requirements = [];
  $lowercase = \Drupal::config('file.settings')->get('filename_sanitization.lowercase');
  if ($lowercase && !Settings::get('file_sanitization_lowercase_extension', FALSE)) {
    $requirements['file_sanitization_lowercase_extension'] = [
      'title' => t('Filename sanitization'),
      'value' => t('File extensions are not converted to lowercase'),
      'description' => t('"Convert to lowercase" is enabled, but file extensions of uploaded files keep their original case (e.g. photo.JPG). Starting with Drupal 13, file extensions will also be converted to lowercase. To adopt this behavior now and remove this warning, add $settings[\'file_sanitization_lowercase_extension\'] = TRUE; to settings.php. See the change record for details.', [
        ':url' => 'https://www.drupal.org/node/[change-record]',
      ]),
      'severity' => RequirementSeverity::Warning,
    ];
  }
  return $requirements;
}

Sites that haven't enabled "Convert to lowercase" are unaffected and see no warning.

3. Drupal 13: make it the default

In Drupal 13, always lowercase the extension when "Convert to lowercase" is enabled. Remove the setting check, the status report warning and the default.settings.php entry. A follow-up issue will track this.

Remaining tasks

  • Agree on the approach and the setting name.
  • Implement the setting check in FileEventSubscriber::sanitizeFilename().
  • Document the setting in default.settings.php.
  • Add the runtime requirement (status report warning) to the File module.
  • Add tests: extend SanitizeNameTest with cases for the setting on and off, and add a test for the status report warning.
  • Write a change record.
  • Create a follow-up issue to make this the default and remove the setting in Drupal 13.

User interface changes

A new warning on the status report (/admin/reports/status) when "Convert to lowercase" is enabled and the new setting isn't.

API changes

New $settings['file_sanitization_lowercase_extension'] setting (default FALSE). When it is enabled, subscribers to FileUploadSanitizeNameEvent that run after core's subscriber receive a filename with a lowercased extension.

Data model changes

None.

Release notes snippet

A new setting, $settings['file_sanitization_lowercase_extension'], lets sites with "Convert to lowercase" filename sanitization enabled also convert file extensions to lowercase. Sites using "Convert to lowercase" without this setting will see a warning on the status report. Starting with Drupal 13, file extensions will always be converted to lowercase when "Convert to lowercase" is enabled.

Comments

eelkeblok created an issue.