You are here

function _file_save_upload_single in Drupal 9

Same name and namespace in other branches
  1. 8 core/modules/file/file.module \_file_save_upload_single()

Saves a file upload to a new location.

@internal This method should only be called from file_save_upload(). Use that method instead.

Parameters

\SplFileInfo $file_info: The file upload to save.

string $form_field_name: A string that is the associative array key of the upload form element in the form array.

array $validators: (optional) An associative array of callback functions used to validate the file.

bool $destination: (optional) A string containing the URI that the file should be copied to.

int $replace: (optional) The replace behavior when the destination file already exists.

Return value

\Drupal\file\FileInterface|false The created file entity or FALSE if the uploaded file not saved.

Throws

\Drupal\Core\Entity\EntityStorageException

See also

file_save_upload()

1 call to _file_save_upload_single()
file_save_upload in core/modules/file/file.module
Saves file uploads to a new location.

File

core/modules/file/file.module, line 933
Defines a "managed_file" Form API field and a "file" field for Field module.

Code

function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $validators = [], $destination = FALSE, $replace = FileSystemInterface::EXISTS_REPLACE) {
  $user = \Drupal::currentUser();

  // Remember the original filename so we can print a message if it changes.
  $original_file_name = $file_info
    ->getClientOriginalName();

  // Check for file upload errors and return FALSE for this file if a lower
  // level system error occurred. For a complete list of errors:
  // See http://php.net/manual/features.file-upload.errors.php.
  switch ($file_info
    ->getError()) {
    case UPLOAD_ERR_INI_SIZE:
    case UPLOAD_ERR_FORM_SIZE:
      \Drupal::messenger()
        ->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', [
        '%file' => $original_file_name,
        '%maxsize' => format_size(Environment::getUploadMaxSize()),
      ]));
      return FALSE;
    case UPLOAD_ERR_PARTIAL:
    case UPLOAD_ERR_NO_FILE:
      \Drupal::messenger()
        ->addError(t('The file %file could not be saved because the upload did not complete.', [
        '%file' => $original_file_name,
      ]));
      return FALSE;
    case UPLOAD_ERR_OK:

      // Final check that this is a valid upload, if it isn't, use the
      // default error handler.
      if (is_uploaded_file($file_info
        ->getRealPath())) {
        break;
      }
    default:

      // Unknown error
      \Drupal::messenger()
        ->addError(t('The file %file could not be saved. An unknown error has occurred.', [
        '%file' => $original_file_name,
      ]));
      return FALSE;
  }

  // Build a list of allowed extensions.
  $extensions = '';
  if (isset($validators['file_validate_extensions'])) {
    if (isset($validators['file_validate_extensions'][0])) {

      // Build the list of non-munged extensions if the caller provided them.
      $extensions = $validators['file_validate_extensions'][0];
    }
    else {

      // If 'file_validate_extensions' is set and the list is empty then the
      // caller wants to allow any extension. In this case we have to remove the
      // validator or else it will reject all extensions.
      unset($validators['file_validate_extensions']);
    }
  }
  else {

    // No validator was provided, so add one using the default list.
    // Build a default non-munged safe list for
    // \Drupal\system\EventSubscriber\SecurityFileUploadEventSubscriber::sanitizeName().
    $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
    $validators['file_validate_extensions'] = [];
    $validators['file_validate_extensions'][0] = $extensions;
  }

  // If the destination is not provided, use the temporary directory.
  if (empty($destination)) {
    $destination = 'temporary://';
  }

  /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');

  // Assert that the destination contains a valid stream.
  $destination_scheme = $stream_wrapper_manager::getScheme($destination);
  if (!$stream_wrapper_manager
    ->isValidScheme($destination_scheme)) {
    \Drupal::messenger()
      ->addError(t('The file could not be uploaded because the destination %destination is invalid.', [
      '%destination' => $destination,
    ]));
    return FALSE;
  }

  // A file URI may already have a trailing slash or look like "public://".
  if (substr($destination, -1) != '/') {
    $destination .= '/';
  }

  // Call an event to sanitize the filename and to attempt to address security
  // issues caused by common server setups.
  $event = new FileUploadSanitizeNameEvent($original_file_name, $extensions);
  \Drupal::service('event_dispatcher')
    ->dispatch($event);

  // Begin building the file entity.
  $values = [
    'uid' => $user
      ->id(),
    'status' => 0,
    // This will be replaced later with a filename based on the destination.
    'filename' => $event
      ->getFilename(),
    'uri' => $file_info
      ->getRealPath(),
    'filesize' => $file_info
      ->getSize(),
  ];
  $file = File::create($values);

  /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  $file_system = \Drupal::service('file_system');
  try {

    // Use the result of the sanitization event as the destination name.
    $file->destination = $file_system
      ->getDestinationFilename($destination . $event
      ->getFilename(), $replace);
  } catch (FileException $e) {
    \Drupal::messenger()
      ->addError(t('The file %filename could not be uploaded because the name is invalid.', [
      '%filename' => $file
        ->getFilename(),
    ]));
    return FALSE;
  }
  $guesser = \Drupal::service('file.mime_type.guesser');
  if ($guesser instanceof MimeTypeGuesserInterface) {
    $file
      ->setMimeType($guesser
      ->guessMimeType($values['filename']));
  }
  else {
    $file
      ->setMimeType($guesser
      ->guess($values['filename']));
    @trigger_error('\\Symfony\\Component\\HttpFoundation\\File\\MimeType\\MimeTypeGuesserInterface is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Implement \\Symfony\\Component\\Mime\\MimeTypeGuesserInterface instead. See https://www.drupal.org/node/3133341', E_USER_DEPRECATED);
  }
  $file->source = $form_field_name;

  // If the destination is FALSE then $replace === FILE_EXISTS_ERROR and
  // there's an existing file, so we need to bail.
  if ($file->destination === FALSE) {
    \Drupal::messenger()
      ->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', [
      '%source' => $form_field_name,
      '%directory' => $destination,
    ]));
    return FALSE;
  }

  // Add in our check of the file name length.
  $validators['file_validate_name_length'] = [];

  // Call the validation functions specified by this function's caller.
  $errors = file_validate($file, $validators);

  // Check for errors.
  if (!empty($errors)) {
    $message = [
      'error' => [
        '#markup' => t('The specified file %name could not be uploaded.', [
          '%name' => $file
            ->getFilename(),
        ]),
      ],
      'item_list' => [
        '#theme' => 'item_list',
        '#items' => $errors,
      ],
    ];

    // @todo Add support for render arrays in
    // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
    // @see https://www.drupal.org/node/2505497.
    \Drupal::messenger()
      ->addError(\Drupal::service('renderer')
      ->renderPlain($message));
    return FALSE;
  }
  $file
    ->setFileUri($file->destination);
  if (!$file_system
    ->moveUploadedFile($file_info
    ->getRealPath(), $file
    ->getFileUri())) {
    \Drupal::messenger()
      ->addError(t('File upload error. Could not move uploaded file.'));
    \Drupal::logger('file')
      ->notice('Upload error. Could not move uploaded file %file to destination %destination.', [
      '%file' => $file
        ->getFilename(),
      '%destination' => $file
        ->getFileUri(),
    ]);
    return FALSE;
  }

  // Update the filename with any changes as a result of the renaming due to an
  // existing file.
  $file
    ->setFilename(\Drupal::service('file_system')
    ->basename($file->destination));

  // If the filename has been modified, let the user know.
  if ($file
    ->getFilename() !== $original_file_name) {
    if ($event
      ->isSecurityRename()) {
      $message = t('For security reasons, your upload has been renamed to %filename.', [
        '%filename' => $file
          ->getFilename(),
      ]);
    }
    else {
      $message = t('Your upload has been renamed to %filename.', [
        '%filename' => $file
          ->getFilename(),
      ]);
    }
    \Drupal::messenger()
      ->addStatus($message);
  }

  // Set the permissions on the new file.
  $file_system
    ->chmod($file
    ->getFileUri());

  // If we are replacing an existing file re-use its database record.
  // @todo Do not create a new entity in order to update it. See
  //   https://www.drupal.org/node/2241865.
  if ($replace == FileSystemInterface::EXISTS_REPLACE) {
    $existing_files = \Drupal::entityTypeManager()
      ->getStorage('file')
      ->loadByProperties([
      'uri' => $file
        ->getFileUri(),
    ]);
    if (count($existing_files)) {
      $existing = reset($existing_files);
      $file->fid = $existing
        ->id();
      $file
        ->setOriginalId($existing
        ->id());
    }
  }

  // Update the filename with any changes as a result of security or renaming
  // due to an existing file.
  $file
    ->setFilename(\Drupal::service('file_system')
    ->basename($file->destination));

  // We can now validate the file object itself before it's saved.
  $violations = $file
    ->validate();
  foreach ($violations as $violation) {
    $errors[] = $violation
      ->getMessage();
  }
  if (!empty($errors)) {
    $message = [
      'error' => [
        '#markup' => t('The specified file %name could not be uploaded.', [
          '%name' => $file
            ->getFilename(),
        ]),
      ],
      'item_list' => [
        '#theme' => 'item_list',
        '#items' => $errors,
      ],
    ];

    // @todo Add support for render arrays in
    // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
    // @see https://www.drupal.org/node/2505497.
    \Drupal::messenger()
      ->addError(\Drupal::service('renderer')
      ->renderPlain($message));
    return FALSE;
  }

  // If we made it this far it's safe to record this file in the database.
  $file
    ->save();

  // Allow an anonymous user who creates a non-public file to see it. See
  // \Drupal\file\FileAccessControlHandler::checkAccess().
  if ($user
    ->isAnonymous() && $destination_scheme !== 'public') {
    $session = \Drupal::request()
      ->getSession();
    $allowed_temp_files = $session
      ->get('anonymous_allowed_file_ids', []);
    $allowed_temp_files[$file
      ->id()] = $file
      ->id();
    $session
      ->set('anonymous_allowed_file_ids', $allowed_temp_files);
  }
  return $file;
}