Issue #3375447 by kim.pepper, larowlan: Create an UploadedFile validator and deprecate error checking methods on UploadedFileInterface
parent
c009376247
commit
6bd3ad841f
|
@ -974,6 +974,10 @@ services:
|
|||
parent: default_plugin_manager
|
||||
tags:
|
||||
- { name: plugin_manager_cache_clear }
|
||||
validation.basic_recursive_validator_factory:
|
||||
class: Drupal\Core\Validation\BasicRecursiveValidatorFactory
|
||||
arguments: ['@class_resolver']
|
||||
Drupal\Core\Validation\BasicRecursiveValidatorFactory: '@validation.basic_recursive_validator_factory'
|
||||
lock:
|
||||
class: Drupal\Core\Lock\DatabaseLockBackend
|
||||
arguments: ['@database']
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Core\Validation;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\TypedData\Validation\ExecutionContextFactory;
|
||||
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
|
||||
use Symfony\Component\Validator\Validator\RecursiveValidator;
|
||||
|
||||
/**
|
||||
* A factory for creating Symfony recursive validators.
|
||||
*/
|
||||
class BasicRecursiveValidatorFactory {
|
||||
|
||||
/**
|
||||
* Constructs a new RecursiveValidatorFactory.
|
||||
*
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $classResolver
|
||||
* The class resolver.
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly ClassResolverInterface $classResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new RecursiveValidator.
|
||||
*
|
||||
* @return \Symfony\Component\Validator\Validator\RecursiveValidator
|
||||
* The validator.
|
||||
*/
|
||||
public function createValidator(): RecursiveValidator {
|
||||
return new RecursiveValidator(
|
||||
new ExecutionContextFactory(new DrupalTranslator()),
|
||||
new LazyLoadingMetadataFactory(),
|
||||
new ConstraintValidatorFactory($this->classResolver),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
* Defines a "managed_file" Form API field and a "file" field for Field module.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\Environment;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Datetime\Entity\DateFormat;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
|
@ -27,13 +26,7 @@ use Drupal\Core\Template\Attribute;
|
|||
use Drupal\Core\Url;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\file\FileInterface;
|
||||
use Drupal\file\Upload\FileValidationException;
|
||||
use Drupal\file\Upload\FormUploadedFile;
|
||||
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;
|
||||
|
||||
// cspell:ignore abiword widthx
|
||||
|
||||
|
@ -651,11 +644,41 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
|
|||
/** @var \Drupal\Core\Render\RendererInterface $renderer */
|
||||
$renderer = \Drupal::service('renderer');
|
||||
$files = [];
|
||||
/** @var \Drupal\file\Validation\UploadedFileValidatorInterface $uploaded_file_validator */
|
||||
$uploaded_file_validator = \Drupal::service('file.uploaded_file_validator');
|
||||
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $uploaded_file */
|
||||
foreach ($uploaded_files as $i => $uploaded_file) {
|
||||
try {
|
||||
$violations = $uploaded_file_validator->validate($uploaded_file);
|
||||
if (count($violations) > 0) {
|
||||
// We only get one violation for uploaded files.
|
||||
\Drupal::messenger()->addError($violations->get(0)->getMessage());
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
}
|
||||
$form_uploaded_file = new FormUploadedFile($uploaded_file);
|
||||
$result = $file_upload_handler->handleFileUpload($form_uploaded_file, $validators, $destination, $replace);
|
||||
$result = $file_upload_handler->handleFileUpload($form_uploaded_file, $validators, $destination, $replace, FALSE);
|
||||
if ($result->hasViolations()) {
|
||||
$errors = [];
|
||||
foreach ($result->getViolations() as $violation) {
|
||||
$errors[] = $violation->getMessage();
|
||||
}
|
||||
$message = [
|
||||
'error' => [
|
||||
'#markup' => t('The specified file %name could not be uploaded.', ['%name' => $uploaded_file->getClientOriginalName()]),
|
||||
],
|
||||
'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($renderer->renderPlain($message));
|
||||
$files[$i] = FALSE;
|
||||
continue;
|
||||
}
|
||||
$file = $result->getFile();
|
||||
// If the filename has been modified, let the user know.
|
||||
if ($result->isRenamed()) {
|
||||
|
@ -677,39 +700,6 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
|
|||
\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' => ByteSizeMarkup::create(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()]);
|
||||
|
|
|
@ -28,3 +28,7 @@ services:
|
|||
class: Drupal\file\Validation\FileValidator
|
||||
arguments: ['@file.recursive_validator', '@validation.constraint', '@event_dispatcher', '@module_handler']
|
||||
Drupal\file\Validation\FileValidatorInterface: '@file.validator'
|
||||
file.uploaded_file_validator:
|
||||
class: Drupal\file\Validation\UploadedFileValidator
|
||||
arguments: ['@validation.basic_recursive_validator_factory']
|
||||
Drupal\file\Validation\UploadedFileValidatorInterface: '@file.uploaded_file_validator'
|
||||
|
|
|
@ -156,47 +156,58 @@ class FileUploadHandler {
|
|||
* - FileSystemInterface::EXISTS_RENAME - Append _{incrementing number}
|
||||
* until the filename is unique.
|
||||
* - FileSystemInterface::EXISTS_ERROR - Throw an exception.
|
||||
* @param bool $throw
|
||||
* (optional) Whether to throw an exception if the file is invalid.
|
||||
*
|
||||
* @return \Drupal\file\Upload\FileUploadResult
|
||||
* The created file entity.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpFoundation\File\Exception\FileException
|
||||
* Thrown when a file upload error occurred.
|
||||
* Thrown when a file upload error occurred and $throws is TRUE.
|
||||
* @throws \Drupal\Core\File\Exception\FileWriteException
|
||||
* Thrown when there is an error moving the file.
|
||||
* Thrown when there is an error moving the file and $throws is TRUE.
|
||||
* @throws \Drupal\Core\File\Exception\FileException
|
||||
* Thrown when a file system error occurs.
|
||||
* Thrown when a file system error occurs and $throws is TRUE.
|
||||
* @throws \Drupal\file\Upload\FileValidationException
|
||||
* Thrown when file validation fails.
|
||||
* Thrown when file validation fails and $throws is TRUE.
|
||||
*/
|
||||
public function handleFileUpload(UploadedFileInterface $uploadedFile, array $validators = [], string $destination = 'temporary://', int $replace = FileSystemInterface::EXISTS_REPLACE): FileUploadResult {
|
||||
public function handleFileUpload(UploadedFileInterface $uploadedFile, array $validators = [], string $destination = 'temporary://', int $replace = FileSystemInterface::EXISTS_REPLACE, bool $throw = TRUE): FileUploadResult {
|
||||
$originalName = $uploadedFile->getClientOriginalName();
|
||||
|
||||
if (!$uploadedFile->isValid()) {
|
||||
// @phpstan-ignore-next-line
|
||||
if ($throw && !$uploadedFile->isValid()) {
|
||||
@trigger_error('Calling ' . __METHOD__ . '() with the $throw argument as TRUE is deprecated in drupal:10.3.0 and will be removed in drupal:11.0.0. Use \Drupal\file\Upload\FileUploadResult::getViolations() instead. See https://www.drupal.org/node/3375456', E_USER_DEPRECATED);
|
||||
// @phpstan-ignore-next-line
|
||||
switch ($uploadedFile->getError()) {
|
||||
case \UPLOAD_ERR_INI_SIZE:
|
||||
// @phpstan-ignore-next-line
|
||||
throw new IniSizeFileException($uploadedFile->getErrorMessage());
|
||||
|
||||
case \UPLOAD_ERR_FORM_SIZE:
|
||||
// @phpstan-ignore-next-line
|
||||
throw new FormSizeFileException($uploadedFile->getErrorMessage());
|
||||
|
||||
case \UPLOAD_ERR_PARTIAL:
|
||||
// @phpstan-ignore-next-line
|
||||
throw new PartialFileException($uploadedFile->getErrorMessage());
|
||||
|
||||
case \UPLOAD_ERR_NO_FILE:
|
||||
// @phpstan-ignore-next-line
|
||||
throw new NoFileException($uploadedFile->getErrorMessage());
|
||||
|
||||
case \UPLOAD_ERR_CANT_WRITE:
|
||||
// @phpstan-ignore-next-line
|
||||
throw new CannotWriteFileException($uploadedFile->getErrorMessage());
|
||||
|
||||
case \UPLOAD_ERR_NO_TMP_DIR:
|
||||
// @phpstan-ignore-next-line
|
||||
throw new NoTmpDirFileException($uploadedFile->getErrorMessage());
|
||||
|
||||
case \UPLOAD_ERR_EXTENSION:
|
||||
// @phpstan-ignore-next-line
|
||||
throw new ExtensionFileException($uploadedFile->getErrorMessage());
|
||||
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
throw new FileException($uploadedFile->getErrorMessage());
|
||||
}
|
||||
|
||||
|
@ -239,14 +250,23 @@ class FileUploadHandler {
|
|||
// Add in our check of the file name length.
|
||||
$validators['FileNameLength'] = [];
|
||||
|
||||
$result = new FileUploadResult();
|
||||
|
||||
// Call the validation functions specified by this function's caller.
|
||||
$violations = $this->fileValidator->validate($file, $validators);
|
||||
$errors = [];
|
||||
foreach ($violations as $violation) {
|
||||
$errors[] = $violation->getMessage();
|
||||
if (count($violations) > 0) {
|
||||
$result->addViolations($violations);
|
||||
return $result;
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
throw new FileValidationException('File validation failed', $filename, $errors);
|
||||
|
||||
if ($throw) {
|
||||
$errors = [];
|
||||
foreach ($violations as $violation) {
|
||||
$errors[] = $violation->getMessage();
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
throw new FileValidationException('File validation failed', $filename, $errors);
|
||||
}
|
||||
}
|
||||
|
||||
$file->setFileUri($destinationFilename);
|
||||
|
@ -267,8 +287,7 @@ class FileUploadHandler {
|
|||
}
|
||||
}
|
||||
|
||||
$result = (new FileUploadResult())
|
||||
->setOriginalFilename($originalName)
|
||||
$result->setOriginalFilename($originalName)
|
||||
->setSanitizedFilename($filename)
|
||||
->setFile($file);
|
||||
|
||||
|
@ -282,11 +301,17 @@ class FileUploadHandler {
|
|||
|
||||
// We can now validate the file object itself before it's saved.
|
||||
$violations = $file->validate();
|
||||
foreach ($violations as $violation) {
|
||||
$errors[] = $violation->getMessage();
|
||||
if ($throw) {
|
||||
foreach ($violations as $violation) {
|
||||
$errors[] = $violation->getMessage();
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
throw new FileValidationException('File validation failed', $filename, $errors);
|
||||
}
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
throw new FileValidationException('File validation failed', $filename, $errors);
|
||||
if (count($violations) > 0) {
|
||||
$result->addViolations($violations);
|
||||
return $result;
|
||||
}
|
||||
|
||||
// If we made it this far it's safe to record this file in the database.
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
namespace Drupal\file\Upload;
|
||||
|
||||
use Drupal\file\FileInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
|
||||
/**
|
||||
* Value object for a file upload result.
|
||||
|
@ -37,6 +40,20 @@ class FileUploadResult {
|
|||
*/
|
||||
protected $file;
|
||||
|
||||
/**
|
||||
* The constraint violations.
|
||||
*
|
||||
* @var \Symfony\Component\Validator\ConstraintViolationListInterface
|
||||
*/
|
||||
protected ConstraintViolationListInterface $violations;
|
||||
|
||||
/**
|
||||
* Creates a new FileUploadResult.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->violations = new ConstraintViolationList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags the result as having had a security rename.
|
||||
*
|
||||
|
@ -131,4 +148,41 @@ class FileUploadResult {
|
|||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a constraint violation.
|
||||
*
|
||||
* @param \Symfony\Component\Validator\ConstraintViolationInterface $violation
|
||||
* The constraint violation.
|
||||
*/
|
||||
public function addViolation(ConstraintViolationInterface $violation): void {
|
||||
$this->violations->add($violation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds constraint violations.
|
||||
*
|
||||
* @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
|
||||
* The constraint violations.
|
||||
*/
|
||||
public function addViolations(ConstraintViolationListInterface $violations): void {
|
||||
$this->violations->addAll($violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint violations.
|
||||
*
|
||||
* @return \Symfony\Component\Validator\ConstraintViolationListInterface
|
||||
* The constraint violations.
|
||||
*/
|
||||
public function getViolations(): ConstraintViolationListInterface {
|
||||
return $this->violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if there are constraint violations.
|
||||
*/
|
||||
public function hasViolations(): bool {
|
||||
return $this->violations->count() > 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,22 +35,43 @@ class FormUploadedFile implements UploadedFileInterface {
|
|||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
|
||||
* \Drupal\file\Validation\UploadedFileValidatorInterface::validate()
|
||||
* instead.
|
||||
*
|
||||
* @see https://www.drupal.org/node/3375456
|
||||
*/
|
||||
public function isValid(): bool {
|
||||
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\file\Validation\UploadedFileValidatorInterface::validate() instead. See https://www.drupal.org/node/3375456', E_USER_DEPRECATED);
|
||||
return $this->uploadedFile->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
|
||||
* \Drupal\file\Validation\UploadedFileValidatorInterface::validate()
|
||||
* instead.
|
||||
*
|
||||
* @see https://www.drupal.org/node/3375456
|
||||
*/
|
||||
public function getErrorMessage(): string {
|
||||
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\file\Validation\UploadedFileValidatorInterface::validate() instead. See https://www.drupal.org/node/3375456', E_USER_DEPRECATED);
|
||||
return $this->uploadedFile->getErrorMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
|
||||
* \Drupal\file\Validation\UploadedFileValidatorInterface::validate()
|
||||
* instead.
|
||||
*
|
||||
* @see https://www.drupal.org/node/3375456
|
||||
*/
|
||||
public function getError(): int {
|
||||
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\file\Validation\UploadedFileValidatorInterface::validate() instead. See https://www.drupal.org/node/3375456', E_USER_DEPRECATED);
|
||||
return $this->uploadedFile->getError();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,11 @@ interface UploadedFileInterface {
|
|||
*
|
||||
* @return bool
|
||||
* TRUE if the file has been uploaded with HTTP and no error occurred.
|
||||
*
|
||||
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
|
||||
* \Drupal\file\Validation\UploadedFileValidatorInterface::validate()
|
||||
* instead.
|
||||
* @see https://www.drupal.org/node/3375456
|
||||
*/
|
||||
public function isValid(): bool;
|
||||
|
||||
|
@ -31,6 +36,12 @@ interface UploadedFileInterface {
|
|||
*
|
||||
* @return string
|
||||
* The error message regarding a failed upload.
|
||||
*
|
||||
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
|
||||
* \Drupal\file\Validation\UploadedFileValidatorInterface::validate()
|
||||
* instead.
|
||||
*
|
||||
* @see https://www.drupal.org/node/3375456
|
||||
*/
|
||||
public function getErrorMessage(): string;
|
||||
|
||||
|
@ -42,6 +53,12 @@ interface UploadedFileInterface {
|
|||
*
|
||||
* @return int
|
||||
* The upload error code.
|
||||
*
|
||||
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
|
||||
* \Drupal\file\Validation\UploadedFileValidatorInterface::validate()
|
||||
* instead.
|
||||
*
|
||||
* @see https://www.drupal.org/node/3375456
|
||||
*/
|
||||
public function getError(): int;
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\file\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* A constraint for UploadedFile objects.
|
||||
*/
|
||||
class UploadedFileConstraint extends Constraint {
|
||||
|
||||
/**
|
||||
* The upload max size. Defaults to checking the environment.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public ?int $maxSize;
|
||||
|
||||
/**
|
||||
* The upload ini size error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $uploadIniSizeErrorMessage = 'The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.';
|
||||
|
||||
/**
|
||||
* The upload form size error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $uploadFormSizeErrorMessage = 'The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.';
|
||||
|
||||
/**
|
||||
* The upload partial error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $uploadPartialErrorMessage = 'The file %file could not be saved because the upload did not complete.';
|
||||
|
||||
/**
|
||||
* The upload no file error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $uploadNoFileErrorMessage = 'The file %file could not be saved because the upload did not complete.';
|
||||
|
||||
/**
|
||||
* The generic file upload error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $uploadErrorMessage = 'The file %file could not be saved. An unknown error has occurred.';
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\file\Validation\Constraint;
|
||||
|
||||
use Drupal\Component\Utility\Environment;
|
||||
use Drupal\Core\StringTranslation\ByteSizeMarkup;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
/**
|
||||
* Constraint validator for uploaded files.
|
||||
*
|
||||
* Use FileValidatorInterface for validating file entities.
|
||||
*
|
||||
* @see \Drupal\Core\Validation\FileValidatorInterface
|
||||
*/
|
||||
class UploadedFileConstraintValidator extends ConstraintValidator {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(mixed $value, Constraint $constraint) {
|
||||
if (!$constraint instanceof UploadedFileConstraint) {
|
||||
throw new UnexpectedTypeException($constraint, UploadedFileConstraint::class);
|
||||
}
|
||||
if (!$value instanceof UploadedFile) {
|
||||
throw new UnexpectedTypeException($value, UploadedFile::class);
|
||||
}
|
||||
if ($value->isValid()) {
|
||||
return;
|
||||
}
|
||||
$maxSize = $constraint->maxSize ?? Environment::getUploadMaxSize();
|
||||
|
||||
match ($value->getError()) {
|
||||
\UPLOAD_ERR_INI_SIZE => $this->context->buildViolation($constraint->uploadIniSizeErrorMessage, [
|
||||
'%file' => $value->getClientOriginalName(),
|
||||
'%maxsize' => ByteSizeMarkup::create($maxSize),
|
||||
])->setCode((string) \UPLOAD_ERR_INI_SIZE)
|
||||
->addViolation(),
|
||||
|
||||
\UPLOAD_ERR_FORM_SIZE => $this->context->buildViolation($constraint->uploadFormSizeErrorMessage, [
|
||||
'%file' => $value->getClientOriginalName(),
|
||||
'%maxsize' => ByteSizeMarkup::create($maxSize),
|
||||
])->setCode((string) \UPLOAD_ERR_FORM_SIZE)
|
||||
->addViolation(),
|
||||
|
||||
\UPLOAD_ERR_PARTIAL => $this->context->buildViolation($constraint->uploadPartialErrorMessage, [
|
||||
'%file' => $value->getClientOriginalName(),
|
||||
])->setCode((string) \UPLOAD_ERR_PARTIAL)
|
||||
->addViolation(),
|
||||
|
||||
\UPLOAD_ERR_NO_FILE => $this->context->buildViolation($constraint->uploadNoFileErrorMessage, [
|
||||
'%file' => $value->getClientOriginalName(),
|
||||
])->setCode((string) \UPLOAD_ERR_NO_FILE)
|
||||
->addViolation(),
|
||||
|
||||
default => $this->context->buildViolation($constraint->uploadErrorMessage, [
|
||||
'%file' => $value->getClientOriginalName(),
|
||||
])->setCode((string) $value->getError())
|
||||
->addViolation()
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\file\Validation;
|
||||
|
||||
use Drupal\Core\Validation\BasicRecursiveValidatorFactory;
|
||||
use Drupal\file\Validation\Constraint\UploadedFileConstraint;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* Validator for uploaded files.
|
||||
*/
|
||||
class UploadedFileValidator implements UploadedFileValidatorInterface {
|
||||
|
||||
/**
|
||||
* The symfony validator.
|
||||
*
|
||||
* @var \Symfony\Component\Validator\Validator\ValidatorInterface
|
||||
*/
|
||||
protected ValidatorInterface $validator;
|
||||
|
||||
/**
|
||||
* Creates a new UploadedFileValidator.
|
||||
*
|
||||
* @param \Drupal\Core\Validation\BasicRecursiveValidatorFactory $validatorFactory
|
||||
* The validator factory.
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly BasicRecursiveValidatorFactory $validatorFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(UploadedFile $uploadedFile, array $options = []): ConstraintViolationListInterface {
|
||||
$constraint = new UploadedFileConstraint($options);
|
||||
return $this->getValidator()->validate($uploadedFile, $constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Symfony validator instance.
|
||||
*
|
||||
* @return \Symfony\Component\Validator\Validator\ValidatorInterface
|
||||
* The Symfony validator.
|
||||
*/
|
||||
protected function getValidator(): ValidatorInterface {
|
||||
if (!isset($this->validator)) {
|
||||
$this->validator = $this->validatorFactory->createValidator();
|
||||
}
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\file\Validation;
|
||||
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
|
||||
/**
|
||||
* Validator for uploaded files.
|
||||
*/
|
||||
interface UploadedFileValidatorInterface {
|
||||
|
||||
/**
|
||||
* Validates an uploaded file.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $uploadedFile
|
||||
* The uploaded file.
|
||||
* @param array $options
|
||||
* An array of options accepted by
|
||||
* \Drupal\file\Validation\Constraint\UploadedFileConstraint.
|
||||
*
|
||||
* @return \Symfony\Component\Validator\ConstraintViolationListInterface
|
||||
* The constraint violation list.
|
||||
*/
|
||||
public function validate(UploadedFile $uploadedFile, array $options = []): ConstraintViolationListInterface;
|
||||
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\file\Kernel;
|
||||
|
||||
use Drupal\Component\Utility\Environment;
|
||||
use Drupal\Core\StringTranslation\ByteSizeMarkup;
|
||||
use Drupal\file\Upload\UploadedFileInterface;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
|
||||
|
||||
/**
|
||||
* 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(UploadedFileInterface::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, ByteSizeMarkup::create(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, ByteSizeMarkup::create(Environment::getUploadMaxSize())));
|
||||
$this->fileUploadHandler->handleFileUpload($file_info);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\file\Kernel\Upload;
|
||||
|
||||
use Drupal\file\Upload\UploadedFileInterface;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
|
||||
/**
|
||||
* Provides tests for legacy file upload handler code.
|
||||
*
|
||||
* @group file
|
||||
* @group legacy
|
||||
* @coversDefaultClass \Drupal\file\Upload\FileUploadHandler
|
||||
*/
|
||||
class LegacyFileUploadHandlerTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['file'];
|
||||
|
||||
/**
|
||||
* @covers ::handleFileUpload
|
||||
*/
|
||||
public function testThrow(): void {
|
||||
$fileUploadHandler = $this->container->get('file.upload_handler');
|
||||
|
||||
$uploadedFile = $this->createMock(UploadedFileInterface::class);
|
||||
$uploadedFile->expects($this->once())
|
||||
->method('isValid')
|
||||
->willReturn(FALSE);
|
||||
|
||||
$this->expectDeprecation('Calling Drupal\file\Upload\FileUploadHandler::handleFileUpload() with the $throw argument as TRUE is deprecated in drupal:10.3.0 and will be removed in drupal:11.0.0. Use \Drupal\file\Upload\FileUploadResult::getViolations() instead. See https://www.drupal.org/node/3375456');
|
||||
$this->expectException(FileException::class);
|
||||
$result = $fileUploadHandler->handleFileUpload(uploadedFile: $uploadedFile, throw: TRUE);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\file\Kernel\Validation;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\file\Validation\UploadedFileValidator;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
/**
|
||||
* Tests the uploaded file validator.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\file\Validation\UploadedFileValidator
|
||||
* @group file
|
||||
*/
|
||||
class UploadedFileValidatorTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['file'];
|
||||
|
||||
/**
|
||||
* The validator under test.
|
||||
*
|
||||
* @var \Drupal\file\Validation\UploadedFileValidator
|
||||
*/
|
||||
protected UploadedFileValidator $validator;
|
||||
|
||||
/**
|
||||
* The file name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $filename;
|
||||
|
||||
/**
|
||||
* The temporary file path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $path;
|
||||
|
||||
/**
|
||||
* The max 4 MB filesize to use for testing.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $maxSize = 4194304;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$fileSystem = $this->container->get('file_system');
|
||||
/** @var \Drupal\file\Validation\UploadedFileValidator $validator */
|
||||
$this->validator = new UploadedFileValidator(
|
||||
$this->container->get('validation.basic_recursive_validator_factory'),
|
||||
);
|
||||
$this->filename = $this->randomMachineName() . '.txt';
|
||||
$this->path = 'temporary://' . $this->filename;
|
||||
|
||||
$fileSystem->saveData('foo', $this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testValidateSuccess(): void {
|
||||
$uploadedFile = new UploadedFile(
|
||||
path: $this->path,
|
||||
originalName: $this->filename,
|
||||
test: TRUE,
|
||||
);
|
||||
$violations = $this->validator->validate($uploadedFile);
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::validate
|
||||
* @dataProvider validateProvider
|
||||
*/
|
||||
public function testValidateFail(int $errorCode, string $message): void {
|
||||
$uploadedFile = new UploadedFile(
|
||||
path: $this->path,
|
||||
originalName: $this->filename,
|
||||
error: $errorCode,
|
||||
test: TRUE,
|
||||
);
|
||||
$violations = $this->validator->validate($uploadedFile, [
|
||||
'maxSize' => $this->maxSize,
|
||||
]);
|
||||
$this->assertCount(1, $violations);
|
||||
$violation = $violations->get(0);
|
||||
$this->assertInstanceOf(TranslatableMarkup::class, $violation->getMessage());
|
||||
$this->assertEquals(sprintf($message, $this->filename), $violation->getMessage());
|
||||
$this->assertEquals($errorCode, $violation->getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testValidateFail.
|
||||
*/
|
||||
public function validateProvider(): array {
|
||||
return [
|
||||
'ini size' => [
|
||||
\UPLOAD_ERR_INI_SIZE,
|
||||
'The file %s could not be saved because it exceeds 4 MB, the maximum allowed size for uploads.',
|
||||
],
|
||||
'form size' => [
|
||||
\UPLOAD_ERR_FORM_SIZE,
|
||||
'The file %s could not be saved because it exceeds 4 MB, the maximum allowed size for uploads.',
|
||||
],
|
||||
'partial file' => [
|
||||
\UPLOAD_ERR_PARTIAL,
|
||||
'The file %s could not be saved because the upload did not complete.',
|
||||
],
|
||||
'no file' => [
|
||||
\UPLOAD_ERR_NO_FILE,
|
||||
'The file %s could not be saved because the upload did not complete.',
|
||||
],
|
||||
'default' => [
|
||||
\UPLOAD_ERR_CANT_WRITE,
|
||||
'The file %s could not be saved. An unknown error has occurred.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue