From bfe527484cb231af34c79549bdeb9e4a5cddbe57 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Fri, 15 Oct 2021 09:01:34 +0100 Subject: [PATCH] Issue #3232248 by kim.pepper, dww, andypost, paulocs, alexpott, mstrelan, Berdir: Move _file_save_upload_single to a service and deprecate --- core/modules/file/file.module | 101 +++++- core/modules/file/file.services.yml | 3 + .../file/src/Upload/FileUploadHandler.php | 336 ++++++++++++++++++ .../file/src/Upload/FileUploadResult.php | 131 +++++++ .../src/Upload/FileValidationException.php | 60 ++++ .../src/Kernel/FileUploadHandlerTest.php | 51 +++ ...{FileModuleTest.php => LegacyFileTest.php} | 4 +- 7 files changed, 678 insertions(+), 8 deletions(-) create mode 100644 core/modules/file/src/Upload/FileUploadHandler.php create mode 100644 core/modules/file/src/Upload/FileUploadResult.php create mode 100644 core/modules/file/src/Upload/FileValidationException.php create mode 100644 core/modules/file/tests/src/Kernel/FileUploadHandlerTest.php rename core/modules/file/tests/src/Kernel/{FileModuleTest.php => LegacyFileTest.php} (84%) diff --git a/core/modules/file/file.module b/core/modules/file/file.module index e702f66c84bc..f17b773d8ca9 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -6,23 +6,32 @@ */ use Drupal\Component\Utility\Environment; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Datetime\Entity\DateFormat; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\File\Event\FileUploadSanitizeNameEvent; use Drupal\Core\File\Exception\FileException; +use Drupal\Core\File\Exception\FileExistsException; +use Drupal\Core\File\Exception\FileWriteException; +use Drupal\Core\File\Exception\InvalidStreamWrapperException; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Link; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Link; +use Drupal\Core\Template\Attribute; use Drupal\Core\Url; use Drupal\file\Entity\File; -use Drupal\Core\File\Event\FileUploadSanitizeNameEvent; use Drupal\file\FileInterface; -use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Template\Attribute; +use Drupal\file\Upload\FileValidationException; +use Symfony\Component\HttpFoundation\File\Exception\FileException as SymfonyFileException; +use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoFileException; +use Symfony\Component\HttpFoundation\File\Exception\PartialFileException; use Symfony\Component\Mime\MimeTypeGuesserInterface; /** @@ -892,9 +901,82 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL $uploaded_files = [$file_upload]; } + if ($destination === FALSE || $destination === NULL) { + $destination = 'temporary://'; + } + + /** @var \Drupal\file\Upload\FileUploadHandler $file_upload_handler */ + $file_upload_handler = \Drupal::service('file.upload_handler'); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); $files = []; - foreach ($uploaded_files as $i => $file_info) { - $files[$i] = _file_save_upload_single($file_info, $form_field_name, $validators, $destination, $replace); + /** @var \Symfony\Component\HttpFoundation\File\UploadedFile $uploaded_file */ + foreach ($uploaded_files as $i => $uploaded_file) { + try { + $result = $file_upload_handler->handleFileUpload($uploaded_file, $validators, $destination, $replace); + $file = $result->getFile(); + // If the filename has been modified, let the user know. + if ($result->isRenamed()) { + if ($result->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); + } + $files[$i] = $file; + } + catch (FileExistsException $e) { + \Drupal::messenger()->addError(t('Destination file "%file" exists', ['%file' => $destination . $uploaded_file->getFilename()])); + $files[$i] = FALSE; + } + catch (InvalidStreamWrapperException $e) { + \Drupal::messenger()->addError(t('The file could not be uploaded because the destination "%destination" is invalid.', ['%destination' => $destination])); + $files[$i] = FALSE; + } + catch (IniSizeFileException | FormSizeFileException $e) { + \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', [ + '%file' => $uploaded_file->getFilename(), + '%maxsize' => format_size(Environment::getUploadMaxSize()), + ])); + $files[$i] = FALSE; + } + catch (PartialFileException | NoFileException $e) { + \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', [ + '%file' => $uploaded_file->getFilename(), + ])); + $files[$i] = FALSE; + } + catch (SymfonyFileException $e) { + \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $uploaded_file->getFilename()])); + $files[$i] = FALSE; + } + catch (FileValidationException $e) { + $message = [ + 'error' => [ + '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $e->getFilename()]), + ], + 'item_list' => [ + '#theme' => 'item_list', + '#items' => $e->getErrors(), + ], + ]; + // @todo Add support for render arrays in + // \Drupal\Core\Messenger\MessengerInterface::addMessage()? + // @see https://www.drupal.org/node/2505497. + \Drupal::messenger()->addError($renderer->renderPlain($message)); + $files[$i] = FALSE; + } + catch (FileWriteException $e) { + \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' => $uploaded_file->getClientOriginalName(), '%destination' => $destination . '/' . $uploaded_file->getClientOriginalName()]); + $files[$i] = FALSE; + } + catch (FileException $e) { + \Drupal::messenger()->addError(t('The file %filename could not be uploaded because the name is invalid.', ['%filename' => $uploaded_file->getClientOriginalName()])); + $files[$i] = FALSE; + } } // Add files to the cache. @@ -928,9 +1010,14 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL * This method should only be called from file_save_upload(). Use that method * instead. * + * @deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use + * \Drupal\file\Upload\FileUploadHandler::handleFileUpload() instead. + * + * @see https://www.drupal.org/node/3239547 * @see file_save_upload() */ function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $validators = [], $destination = FALSE, $replace = FileSystemInterface::EXISTS_REPLACE) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\file\Upload\FileUploadHandler::handleFileUpload() instead. See https://www.drupal.org/node/3239547', E_USER_DEPRECATED); $user = \Drupal::currentUser(); // Remember the original filename so we can print a message if it changes. $original_file_name = $file_info->getClientOriginalName(); diff --git a/core/modules/file/file.services.yml b/core/modules/file/file.services.yml index 037547cb1f5c..5558d383d3d4 100644 --- a/core/modules/file/file.services.yml +++ b/core/modules/file/file.services.yml @@ -4,3 +4,6 @@ services: arguments: ['@config.factory', '@database', 'file_usage'] tags: - { 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' ] diff --git a/core/modules/file/src/Upload/FileUploadHandler.php b/core/modules/file/src/Upload/FileUploadHandler.php new file mode 100644 index 000000000000..bcfc983002cf --- /dev/null +++ b/core/modules/file/src/Upload/FileUploadHandler.php @@ -0,0 +1,336 @@ +<?php + +namespace Drupal\file\Upload; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\File\Event\FileUploadSanitizeNameEvent; +use Drupal\Core\File\Exception\FileExistsException; +use Drupal\Core\File\Exception\FileWriteException; +use Drupal\Core\File\Exception\InvalidStreamWrapperException; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; +use Drupal\file\Entity\File; +use Drupal\file\FileInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; +use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException; +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException; +use Symfony\Component\HttpFoundation\File\Exception\PartialFileException; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Mime\MimeTypeGuesserInterface; + +/** + * Handles validating and creating file entities from file uploads. + */ +class FileUploadHandler { + + /** + * The default extensions if none are provided. + */ + const DEFAULT_EXTENSIONS = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'; + + /** + * The file system service. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The stream wrapper manager. + * + * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface + */ + protected $streamWrapperManager; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The MIME type guesser. + * + * @var \Symfony\Component\Mime\MimeTypeGuesserInterface + */ + protected $mimeTypeGuesser; + + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** + * Constructs a FileUploadHandler object. + * + * @param \Drupal\Core\File\FileSystemInterface $fileSystem + * The file system service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager. + * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager + * The stream wrapper manager. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher + * The event dispatcher. + * @param \Symfony\Component\Mime\MimeTypeGuesserInterface $mimeTypeGuesser + * The MIME type guesser. + * @param \Drupal\Core\Session\AccountInterface $currentUser + * The current user. + * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack + * The request stack. + */ + public function __construct(FileSystemInterface $fileSystem, EntityTypeManagerInterface $entityTypeManager, StreamWrapperManagerInterface $streamWrapperManager, EventDispatcherInterface $eventDispatcher, MimeTypeGuesserInterface $mimeTypeGuesser, AccountInterface $currentUser, RequestStack $requestStack) { + $this->fileSystem = $fileSystem; + $this->entityTypeManager = $entityTypeManager; + $this->streamWrapperManager = $streamWrapperManager; + $this->eventDispatcher = $eventDispatcher; + $this->mimeTypeGuesser = $mimeTypeGuesser; + $this->currentUser = $currentUser; + $this->requestStack = $requestStack; + } + + /** + * Creates a file from an upload. + * + * @param \Symfony\Component\HttpFoundation\File\UploadedFile $uploadedFile + * The uploaded file object. + * @param array $validators + * The validators to run against the uploaded file. + * @param string $destination + * The destination directory. + * @param int $replace + * Replace behavior when the destination file already exists: + * - FileSystemInterface::EXISTS_REPLACE - Replace the existing file. + * - FileSystemInterface::EXISTS_RENAME - Append _{incrementing number} + * until the filename is unique. + * - FileSystemInterface::EXISTS_ERROR - Throw an exception. + * + * @return \Drupal\file\Upload\FileUploadResult + * The created file entity. + * + * @throws \Symfony\Component\HttpFoundation\File\Exception\FileException + * Thrown when a file upload error occurred. + * @throws \Drupal\Core\File\Exception\FileWriteException + * Thrown when there is an error moving the file. + * @throws \Drupal\Core\File\Exception\FileException + * Thrown when a file system error occurs. + * @throws \Drupal\file\Upload\FileValidationException + * Thrown when file validation fails. + */ + public function handleFileUpload(UploadedFile $uploadedFile, array $validators = [], string $destination = 'temporary://', int $replace = FileSystemInterface::EXISTS_REPLACE): FileUploadResult { + $originalName = $uploadedFile->getClientOriginalName(); + + if (!$uploadedFile->isValid()) { + switch ($uploadedFile->getError()) { + case \UPLOAD_ERR_INI_SIZE: + throw new IniSizeFileException($uploadedFile->getErrorMessage()); + + case \UPLOAD_ERR_FORM_SIZE: + throw new FormSizeFileException($uploadedFile->getErrorMessage()); + + case \UPLOAD_ERR_PARTIAL: + throw new PartialFileException($uploadedFile->getErrorMessage()); + + case \UPLOAD_ERR_NO_FILE: + throw new NoFileException($uploadedFile->getErrorMessage()); + + case \UPLOAD_ERR_CANT_WRITE: + throw new CannotWriteFileException($uploadedFile->getErrorMessage()); + + case \UPLOAD_ERR_NO_TMP_DIR: + throw new NoTmpDirFileException($uploadedFile->getErrorMessage()); + + case \UPLOAD_ERR_EXTENSION: + throw new ExtensionFileException($uploadedFile->getErrorMessage()); + + } + + throw new FileException($uploadedFile->getErrorMessage()); + } + + $extensions = $this->handleExtensionValidation($validators); + + // Assert that the destination contains a valid stream. + $destinationScheme = $this->streamWrapperManager::getScheme($destination); + if (!$this->streamWrapperManager->isValidScheme($destinationScheme)) { + throw new InvalidStreamWrapperException(sprintf('The file could not be uploaded because the destination "%s" is invalid.', $destination)); + } + + // 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($originalName, $extensions); + $this->eventDispatcher->dispatch($event); + $filename = $event->getFilename(); + + $mimeType = $this->mimeTypeGuesser->guessMimeType($filename); + $destinationFilename = $this->fileSystem->getDestinationFilename($destination . $filename, $replace); + if ($destinationFilename === FALSE) { + throw new FileExistsException(sprintf('Destination file "%s" exists', $destinationFilename)); + } + + $file = File::create([ + 'uid' => $this->currentUser->id(), + 'status' => 0, + 'uri' => $uploadedFile->getRealPath(), + ]); + + // This will be replaced later with a filename based on the destination. + $file->setFilename($filename); + $file->setMimeType($mimeType); + $file->setSize($uploadedFile->getSize()); + + // 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); + if (!empty($errors)) { + throw new FileValidationException('File validation failed', $filename, $errors); + } + + $file->setFileUri($destinationFilename); + if (!$this->fileSystem->moveUploadedFile($uploadedFile->getRealPath(), $file->getFileUri())) { + throw new FileWriteException('File upload error. Could not move uploaded file.'); + } + + // Update the filename with any changes as a result of security or renaming + // due to an existing file. + $file->setFilename($this->fileSystem->basename($file->getFileUri())); + + if ($replace === FileSystemInterface::EXISTS_REPLACE) { + $existingFile = $this->loadByUri($file->getFileUri()); + if ($existingFile) { + $file->fid = $existingFile->id(); + $file->setOriginalId($existingFile->id()); + } + } + + $result = (new FileUploadResult()) + ->setOriginalFilename($originalName) + ->setSanitizedFilename($filename) + ->setFile($file); + + // If the filename has been modified, let the user know. + if ($event->isSecurityRename()) { + $result->setSecurityRename(); + } + + // Set the permissions on the new file. + $this->fileSystem->chmod($file->getFileUri()); + + // 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)) { + throw new FileValidationException('File validation failed', $filename, $errors); + } + + // 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 ($this->currentUser->isAnonymous() && $destinationScheme !== 'public') { + $session = $this->requestStack->getCurrentRequest()->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 $result; + } + + /** + * Gets the list of allowed extensions and updates the validators. + * + * This will add an extension validator to the list of validators if one is + * not set. + * + * If the extension validator is set, but no extensions are specified, it + * means all extensions are allowed, so the validator is removed from the list + * of validators. + * + * @param array $validators + * The file validators in use. + * + * @return string + * The space delimited list of allowed file extensions. + */ + protected function handleExtensionValidation(array &$validators): string { + // Build a list of allowed extensions. + if (isset($validators['file_validate_extensions'])) { + if (!isset($validators['file_validate_extensions'][0])) { + // 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(). + $validators['file_validate_extensions'] = [self::DEFAULT_EXTENSIONS]; + } + return $validators['file_validate_extensions'][0] ?? ''; + } + + /** + * Loads the first File entity found with the specified URI. + * + * @param string $uri + * The file URI. + * + * @return \Drupal\file\FileInterface|null + * The first file with the matched URI if found, NULL otherwise. + * + * @todo replace with https://www.drupal.org/project/drupal/issues/3223209 + */ + protected function loadByUri(string $uri): ?FileInterface { + $fileStorage = $this->entityTypeManager->getStorage('file'); + /** @var \Drupal\file\FileInterface[] $files */ + $files = $fileStorage->loadByProperties(['uri' => $uri]); + if (count($files)) { + foreach ($files as $item) { + // Since some database servers sometimes use a case-insensitive + // comparison by default, double check that the filename is an exact + // match. + if ($item->getFileUri() === $uri) { + return $item; + } + } + } + return NULL; + } + +} diff --git a/core/modules/file/src/Upload/FileUploadResult.php b/core/modules/file/src/Upload/FileUploadResult.php new file mode 100644 index 000000000000..8481002cf602 --- /dev/null +++ b/core/modules/file/src/Upload/FileUploadResult.php @@ -0,0 +1,131 @@ +<?php + +namespace Drupal\file\Upload; + +use Drupal\file\FileInterface; + +/** + * Value object for a file upload result. + */ +class FileUploadResult { + + /** + * If the filename was renamed for security reasons. + * + * @var bool + */ + protected $securityRename = FALSE; + + /** + * The sanitized filename. + * + * @var string + */ + protected $sanitizedFilename; + + /** + * The original filename. + * + * @var string + */ + protected $originalFilename; + + /** + * The File entity. + * + * @var \Drupal\file\FileInterface + */ + protected $file; + + /** + * Flags the result as having had a security rename. + * + * @return $this + */ + public function setSecurityRename(): FileUploadResult { + $this->securityRename = TRUE; + return $this; + } + + /** + * Sets the sanitized filename. + * + * @param string $sanitizedFilename + * + * @return $this + */ + public function setSanitizedFilename(string $sanitizedFilename): FileUploadResult { + $this->sanitizedFilename = $sanitizedFilename; + return $this; + } + + /** + * Gets the original filename. + * + * @return string + */ + public function getOriginalFilename(): string { + return $this->originalFilename; + } + + /** + * Sets the original filename. + * + * @param string $originalFilename + * + * @return $this + */ + public function setOriginalFilename(string $originalFilename): FileUploadResult { + $this->originalFilename = $originalFilename; + return $this; + } + + /** + * Sets the File entity. + * + * @param \Drupal\file\FileInterface $file + * + * @return $this + */ + public function setFile(FileInterface $file): FileUploadResult { + $this->file = $file; + return $this; + } + + /** + * Returns if there was a security rename. + * + * @return bool + */ + public function isSecurityRename(): bool { + return $this->securityRename; + } + + /** + * Returns if there was a file rename. + * + * @return bool + */ + public function isRenamed(): bool { + return $this->originalFilename !== $this->sanitizedFilename; + } + + /** + * Gets the sanitized filename. + * + * @return string + */ + public function getSanitizedFilename(): string { + return $this->sanitizedFilename; + } + + /** + * Gets the File entity. + * + * @return \Drupal\file\FileInterface + */ + public function getFile(): FileInterface { + return $this->file; + } + +} diff --git a/core/modules/file/src/Upload/FileValidationException.php b/core/modules/file/src/Upload/FileValidationException.php new file mode 100644 index 000000000000..c5bc7b40dd52 --- /dev/null +++ b/core/modules/file/src/Upload/FileValidationException.php @@ -0,0 +1,60 @@ +<?php + +namespace Drupal\file\Upload; + +/** + * Provides an exception for upload validation errors. + */ +class FileValidationException extends \RuntimeException { + + /** + * The validation errors. + * + * @var array + */ + protected $errors; + + /** + * The file name. + * + * @var string + */ + protected $fileName; + + /** + * Constructs a new FileValidationException. + * + * @param string $message + * The message. + * @param string $file_name + * The file name. + * @param array $errors + * The validation errors. + */ + public function __construct(string $message, string $file_name, array $errors) { + parent::__construct($message, 0, NULL); + $this->fileName = $file_name; + $this->errors = $errors; + } + + /** + * Gets the file name. + * + * @return string + * The file name. + */ + public function getFilename(): string { + return $this->fileName; + } + + /** + * Gets the errors. + * + * @return array + * The errors. + */ + public function getErrors(): array { + return $this->errors; + } + +} diff --git a/core/modules/file/tests/src/Kernel/FileUploadHandlerTest.php b/core/modules/file/tests/src/Kernel/FileUploadHandlerTest.php new file mode 100644 index 000000000000..5c8fb7fb0bf0 --- /dev/null +++ b/core/modules/file/tests/src/Kernel/FileUploadHandlerTest.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\Tests\file\Kernel; + +use Drupal\Component\Utility\Environment; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; +use Symfony\Component\HttpFoundation\File\UploadedFile; + +/** + * Tests the file upload handler. + * + * @group file + */ +class FileUploadHandlerTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['file']; + + /** + * The file upload handler under test. + * + * @var \Drupal\file\Upload\FileUploadHandler + */ + protected $fileUploadHandler; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->fileUploadHandler = $this->container->get('file.upload_handler'); + } + + /** + * Tests file size upload errors. + */ + public function testFileSaveUploadSingleErrorFormSize() { + $file_name = $this->randomMachineName(); + $file_info = $this->createMock(UploadedFile::class); + $file_info->expects($this->once())->method('getError')->willReturn(UPLOAD_ERR_FORM_SIZE); + $file_info->expects($this->once())->method('getClientOriginalName')->willReturn($file_name); + $file_info->expects($this->once())->method('getErrorMessage')->willReturn(sprintf('The file "%s" could not be saved because it exceeds %s, the maximum allowed size for uploads.', $file_name, format_size(Environment::getUploadMaxSize()))); + $this->expectException(FormSizeFileException::class); + $this->expectExceptionMessage(sprintf('The file "%s" could not be saved because it exceeds %s, the maximum allowed size for uploads.', $file_name, format_size(Environment::getUploadMaxSize()))); + $this->fileUploadHandler->handleFileUpload($file_info); + } + +} diff --git a/core/modules/file/tests/src/Kernel/FileModuleTest.php b/core/modules/file/tests/src/Kernel/LegacyFileTest.php similarity index 84% rename from core/modules/file/tests/src/Kernel/FileModuleTest.php rename to core/modules/file/tests/src/Kernel/LegacyFileTest.php index beb925f97d75..a3a46257f03e 100644 --- a/core/modules/file/tests/src/Kernel/FileModuleTest.php +++ b/core/modules/file/tests/src/Kernel/LegacyFileTest.php @@ -11,8 +11,9 @@ * Tests file.module methods. * * @group file + * @group legacy */ -class FileModuleTest extends KernelTestBase { +class LegacyFileTest extends KernelTestBase { /** * {@inheritdoc} @@ -29,6 +30,7 @@ public function testFileSaveUploadSingleErrorFormSize() { $file_info = $this->createMock(UploadedFile::class); $file_info->expects($this->once())->method('getError')->willReturn(UPLOAD_ERR_FORM_SIZE); $file_info->expects($this->once())->method('getClientOriginalName')->willReturn($file_name); + $this->expectDeprecation('_file_save_upload_single() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\file\Upload\FileUploadHandler::handleFileUpload() instead. See https://www.drupal.org/node/3239547'); $this->assertFalse(\_file_save_upload_single($file_info, 'name')); $expected_message = new TranslatableMarkup('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_name, '%maxsize' => format_size(Environment::getUploadMaxSize())]); $this->assertEquals($expected_message, \Drupal::messenger()->all()['error'][0]); -- GitLab