Verified Commit 79c7666a authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #3221793 by kim.pepper, larowlan, smustgrave, Wim Leers, joachim,...

Issue #3221793 by kim.pepper, larowlan, smustgrave, Wim Leers, joachim, gabesullice: Move file upload validation from file.module to constraint validators
parent 8219cb1f
Loading
Loading
Loading
Loading
+35 −29
Original line number Diff line number Diff line
@@ -9,24 +9,22 @@
use Drupal\Component\Utility\Environment;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\File\Event\FileUploadSanitizeNameEvent;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\editor\Entity\Editor;
use Drupal\Core\Validation\DrupalTranslator;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\file\Validation\FileValidatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

/**
 * Returns response for CKEditor 5 Simple image upload adapter.
@@ -71,6 +69,13 @@ class CKEditor5ImageController extends ControllerBase {
   */
  protected $eventDispatcher;

  /**
   * The file validator.
   *
   * @var \Drupal\file\Validation\FileValidatorInterface
   */
  protected FileValidatorInterface $fileValidator;

  /**
   * Constructs a new CKEditor5ImageController.
   *
@@ -84,13 +89,20 @@ class CKEditor5ImageController extends ControllerBase {
   *   The lock service.
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\file\Validation\FileValidatorInterface|null $file_validator
   *   The file validator.
   */
  public function __construct(FileSystemInterface $file_system, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher) {
  public function __construct(FileSystemInterface $file_system, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, FileValidatorInterface $file_validator = NULL) {
    $this->fileSystem = $file_system;
    $this->currentUser = $current_user;
    $this->mimeTypeGuesser = $mime_type_guesser;
    $this->lock = $lock;
    $this->eventDispatcher = $event_dispatcher;
    if (!$file_validator) {
      @trigger_error('Calling ' . __METHOD__ . '() without the $file_validator argument is deprecated in drupal:10.2.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
      $file_validator = \Drupal::service('file.validator');
    }
    $this->fileValidator = $file_validator;
  }

  /**
@@ -103,6 +115,7 @@ public static function create(ContainerInterface $container) {
      $container->get('file.mime_type.guesser'),
      $container->get('lock'),
      $container->get('event_dispatcher'),
      $container->get('file.validator')
    );
  }

@@ -144,13 +157,20 @@ public function upload(Request $request) {
      $max_dimensions = 0;
    }

    $allowed_extensions = 'gif png jpg jpeg';
    $validators = [
      'file_validate_extensions' => ['gif png jpg jpeg'],
      'file_validate_size' => [$max_filesize],
      'file_validate_image_resolution' => [$max_dimensions],
      'FileExtension' => [
        'extensions' => $allowed_extensions,
      ],
      'FileSizeLimit' => [
        'fileLimit' => $max_filesize,
      ],
      'FileImageDimensions' => [
        'maxDimensions' => $max_dimensions,
      ],
    ];

    $prepared_filename = $this->prepareFilename($filename, $validators);
    $prepared_filename = $this->prepareFilename($filename, $allowed_extensions);

    // Create the file.
    $file_uri = "{$destination}/{$prepared_filename}";
@@ -225,7 +245,7 @@ public function imageUploadEnabledAccess(Editor $editor) {
   * @param \Drupal\file\FileInterface $file
   *   The file entity to validate.
   * @param array $validators
   *   An array of upload validators to pass to file_validate().
   *   An array of upload validators to pass to the FileValidator.
   *
   * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
   *   The list of constraint violations, if any.
@@ -238,20 +258,7 @@ protected function validate(FileInterface $file, array $validators) {
    $violations->filterByFieldAccess();

    // Validate the file based on the field definition configuration.
    $errors = file_validate($file, $validators);
    if (!empty($errors)) {
      $translator = new DrupalTranslator();
      foreach ($errors as $error) {
        $violation = new ConstraintViolation($translator->trans($error),
          (string) $error,
          [],
          EntityAdapter::createFromEntity($file),
          '',
          NULL
        );
        $violations->add($violation);
      }
    }
    $violations->addAll($this->fileValidator->validate($file, $validators));

    return $violations;
  }
@@ -261,15 +268,14 @@ protected function validate(FileInterface $file, array $validators) {
   *
   * @param string $filename
   *   The file name.
   * @param array $validators
   *   The array of upload validators.
   * @param string $allowed_extensions
   *   The allowed extensions.
   *
   * @return string
   *   The prepared/munged filename.
   */
  protected function prepareFilename($filename, array &$validators) {
    $extensions = $validators['file_validate_extensions'][0] ?? '';
    $event = new FileUploadSanitizeNameEvent($filename, $extensions);
  protected function prepareFilename(string $filename, string $allowed_extensions): string {
    $event = new FileUploadSanitizeNameEvent($filename, $allowed_extensions);
    $this->eventDispatcher->dispatch($event);

    return $event->getFilename();
+3 −3
Original line number Diff line number Diff line
@@ -112,9 +112,9 @@ public function buildForm(array $form, FormStateInterface $form_state, Editor $e
      '#upload_location' => $image_upload['scheme'] . '://' . $image_upload['directory'],
      '#default_value' => $fid ? [$fid] : NULL,
      '#upload_validators' => [
        'file_validate_extensions' => ['gif png jpg jpeg'],
        'file_validate_size' => [$max_filesize],
        'file_validate_image_resolution' => [$max_dimensions],
        'FileExtension' => ['extensions' => 'gif png jpg jpeg'],
        'FileSizeLimit' => ['fileLimit' => $max_filesize],
        'FileImageDimensions' => ['maxDimensions' => $max_dimensions],
      ],
      '#required' => TRUE,
    ];
+23 −4
Original line number Diff line number Diff line
@@ -153,20 +153,39 @@ function template_preprocess_file_upload_help(&$variables) {
    }
  }
  if (isset($upload_validators['file_validate_size'])) {
    @trigger_error('\'file_validate_size\' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'FileSizeLimit\' constraint instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
    $descriptions[] = t('@size limit.', ['@size' => format_size($upload_validators['file_validate_size'][0])]);
  }
  if (isset($upload_validators['FileSizeLimit'])) {
    $descriptions[] = t('@size limit.', ['@size' => format_size($upload_validators['FileSizeLimit']['fileLimit'])]);
  }

  if (isset($upload_validators['file_validate_extensions'])) {
    @trigger_error('\'file_validate_extensions\' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'FileExtension\' constraint instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
    $descriptions[] = t('Allowed types: @extensions.', ['@extensions' => $upload_validators['file_validate_extensions'][0]]);
  }
  if (isset($upload_validators['FileExtension'])) {
    $descriptions[] = t('Allowed types: @extensions.', ['@extensions' => $upload_validators['FileExtension']['extensions']]);
  }

  if (isset($upload_validators['file_validate_image_resolution']) || isset($upload_validators['FileImageDimensions'])) {
    if (isset($upload_validators['file_validate_image_resolution'])) {
      @trigger_error('\'file_validate_image_resolution\' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'FileImageDimensions\' constraint instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
      $max = $upload_validators['file_validate_image_resolution'][0];
      $min = $upload_validators['file_validate_image_resolution'][1];
    }
    else {
      $max = $upload_validators['FileImageDimensions']['maxDimensions'];
      $min = $upload_validators['FileImageDimensions']['minDimensions'];
    }
    if ($min && $max && $min == $max) {
      $descriptions[] = t('Images must be exactly <strong>@size</strong> pixels.', ['@size' => $max]);
    }
    elseif ($min && $max) {
      $descriptions[] = t('Images must be larger than <strong>@min</strong> pixels. Images larger than <strong>@max</strong> pixels will be resized.', ['@min' => $min, '@max' => $max]);
      $descriptions[] = t('Images must be larger than <strong>@min</strong> pixels. Images larger than <strong>@max</strong> pixels will be resized.', [
        '@min' => $min,
        '@max' => $max,
      ]);
    }
    elseif ($min) {
      $descriptions[] = t('Images must be larger than <strong>@min</strong> pixels.', ['@min' => $min]);
+34 −19
Original line number Diff line number Diff line
@@ -94,28 +94,17 @@ function file_field_widget_info_alter(array &$info) {
 * @return array
 *   An array containing validation error messages.
 *
 * @see hook_file_validate()
 * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the
 *   'file.validator' service instead.
 *
 * @see https://www.drupal.org/node/3363700
 */
function file_validate(FileInterface $file, $validators = []) {
  // Call the validation functions specified by this function's caller.
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'file.validator\' service instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
  $violations = \Drupal::service('file.validator')->validate($file, $validators);
  $errors = [];
  foreach ($validators as $function => $args) {
    if (function_exists($function)) {
      array_unshift($args, $file);
      $errors = array_merge($errors, call_user_func_array($function, $args));
    }
  }

  // Let other modules perform validation on the new file.
  $errors = array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));

  // Ensure the file does not contain a malicious extension. At this point
  // \Drupal\file\Upload\FileUploadHandler::handleFileUpload() will have munged
  // the file so it does not contain a malicious extension. Contributed and
  // custom code that calls this method needs to take similar steps if they need
  // to permit files with malicious extensions to be uploaded.
  if (empty($errors) && !\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FileSystemInterface::INSECURE_EXTENSION_REGEX, $file->getFilename())) {
    $errors[] = t('For security reasons, your upload has been rejected.');
  foreach ($violations as $violation) {
    $errors[] = $violation->getMessage();
  }
  return $errors;
}
@@ -129,8 +118,14 @@ function file_validate(FileInterface $file, $validators = []) {
 * @return array
 *   An empty array if the file name length is smaller than the limit or an
 *   array containing an error message if it's not or is empty.
 *
 * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the
 *   'file.validator' service instead.
 *
 * @see https://www.drupal.org/node/3363700
 */
function file_validate_name_length(FileInterface $file) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'file.validator\' service instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
  $errors = [];

  if (!$file->getFilename()) {
@@ -154,9 +149,14 @@ function file_validate_name_length(FileInterface $file) {
 *   An empty array if the file extension is allowed or an array containing an
 *   error message if it's not.
 *
 * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the
 *   'file.validator' service instead.
 *
 * @see https://www.drupal.org/node/3363700
 * @see hook_file_validate()
 */
function file_validate_extensions(FileInterface $file, $extensions) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'file.validator\' service instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
  $errors = [];

  $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
@@ -189,9 +189,14 @@ function file_validate_extensions(FileInterface $file, $extensions) {
 *   An empty array if the file size is below limits or an array containing an
 *   error message if it's not.
 *
 * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the
 *   'file.validator' service instead.
 *
 * @see https://www.drupal.org/node/3363700
 * @see hook_file_validate()
 */
function file_validate_size(FileInterface $file, $file_limit = 0, $user_limit = 0) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'file.validator\' service instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
  $user = \Drupal::currentUser();
  $errors = [];

@@ -217,9 +222,14 @@ function file_validate_size(FileInterface $file, $file_limit = 0, $user_limit =
 *   An empty array if the file is a valid image or an array containing an error
 *   message if it's not.
 *
 * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the
 *   'file.validator' service instead.
 *
 * @see https://www.drupal.org/node/3363700
 * @see hook_file_validate()
 */
function file_validate_is_image(FileInterface $file) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'file.validator\' service instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
  $errors = [];

  $image_factory = \Drupal::service('image.factory');
@@ -256,9 +266,14 @@ function file_validate_is_image(FileInterface $file) {
 *   does not meet the requirements or an attempt to resize it fails, an array
 *   containing the error message will be returned.
 *
 * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the
 *   'file.validator' service instead.
 *
 * @see https://www.drupal.org/node/3363700
 * @see hook_file_validate()
 */
function file_validate_image_resolution(FileInterface $file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
  @trigger_error(__FUNCTION__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use the \'file.validator\' service instead. See https://www.drupal.org/node/3363700', E_USER_DEPRECATED);
  $errors = [];

  // Check first that the file is an image.
+12 −1
Original line number Diff line number Diff line
@@ -6,9 +6,20 @@ services:
      - { name: backend_overridable }
  file.upload_handler:
    class: Drupal\file\Upload\FileUploadHandler
    arguments: [ '@file_system', '@entity_type.manager', '@stream_wrapper_manager', '@event_dispatcher', '@file.mime_type.guesser', '@current_user', '@request_stack', '@file.repository' ]
    arguments: ['@file_system', '@entity_type.manager', '@stream_wrapper_manager', '@event_dispatcher', '@file.mime_type.guesser', '@current_user', '@request_stack', '@file.repository', '@file.validator']
  Drupal\file\Upload\FileUploadHandler: '@file.upload_handler'
  file.repository:
    class: Drupal\file\FileRepository
    arguments: [ '@file_system', '@stream_wrapper_manager', '@entity_type.manager', '@module_handler', '@file.usage', '@current_user' ]
  Drupal\file\FileRepositoryInterface: '@file.repository'
  file.recursive_validator_factory:
    class: Drupal\file\Validation\RecursiveValidatorFactory
    arguments: ['@class_resolver', '@typed_data_manager']
  Drupal\file\Validation\RecursiveValidatorFactory: '@file.recursive_validator_factory'
  file.recursive_validator:
    class: Symfony\Component\Validator\Validator\ValidatorInterface
    factory: ['@file.recursive_validator_factory', 'createValidator']
  file.validator:
    class: Drupal\file\Validation\FileValidator
    arguments: ['@file.recursive_validator', '@validation.constraint', '@event_dispatcher', '@module_handler']
  Drupal\file\Validation\FileValidatorInterface: '@file.validator'
Loading