diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e0fa273fd8..09ca1c5747 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -10652,7 +10652,7 @@ parameters: - message: "#^Access to an undefined property Ibexa\\\\Core\\\\FieldType\\\\Value\\:\\:\\$fileName\\.$#" - count: 2 + count: 1 path: src/lib/FieldType/Validator/FileExtensionBlackListValidator.php - diff --git a/src/lib/FieldType/BinaryBase/BinaryBaseStorage.php b/src/lib/FieldType/BinaryBase/BinaryBaseStorage.php index 71fc3c01d5..7817c92801 100644 --- a/src/lib/FieldType/BinaryBase/BinaryBaseStorage.php +++ b/src/lib/FieldType/BinaryBase/BinaryBaseStorage.php @@ -13,6 +13,8 @@ use Ibexa\Contracts\Core\IO\MimeTypeDetector; use Ibexa\Contracts\Core\Persistence\Content\Field; use Ibexa\Contracts\Core\Persistence\Content\VersionInfo; +use Ibexa\Core\Base\Exceptions\ContentFieldValidationException; +use Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator; use Ibexa\Core\IO\IOServiceInterface; /** @@ -39,24 +41,21 @@ class BinaryBaseStorage extends GatewayBasedStorage /** @var \Ibexa\Core\FieldType\BinaryBase\BinaryBaseStorage\Gateway */ protected $gateway; - /** - * Construct from gateways. - * - * @param \Ibexa\Contracts\Core\FieldType\StorageGatewayInterface $gateway - * @param \Ibexa\Core\IO\IOServiceInterface $ioService - * @param \Ibexa\Contracts\Core\FieldType\BinaryBase\PathGenerator $pathGenerator - * @param \Ibexa\Contracts\Core\IO\MimeTypeDetector $mimeTypeDetector - */ + /** @var \Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator */ + protected $fileExtensionBlackListValidator; + public function __construct( StorageGatewayInterface $gateway, IOServiceInterface $ioService, PathGenerator $pathGenerator, - MimeTypeDetector $mimeTypeDetector + MimeTypeDetector $mimeTypeDetector, + FileExtensionBlackListValidator $fileExtensionBlackListValidator ) { parent::__construct($gateway); $this->ioService = $ioService; $this->pathGenerator = $pathGenerator; $this->mimeTypeDetector = $mimeTypeDetector; + $this->fileExtensionBlackListValidator = $fileExtensionBlackListValidator; } /** @@ -67,6 +66,10 @@ public function setDownloadUrlGenerator(PathGenerator $downloadUrlGenerator) $this->downloadUrlGenerator = $downloadUrlGenerator; } + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\ContentFieldValidationException + */ public function storeFieldData(VersionInfo $versionInfo, Field $field, array $context) { if ($field->value->externalData === null) { @@ -76,6 +79,17 @@ public function storeFieldData(VersionInfo $versionInfo, Field $field, array $co } if (isset($field->value->externalData['inputUri'])) { + $this->fileExtensionBlackListValidator->validateFileExtension($field->value->externalData['fileName']); + if (!empty($errors = $this->fileExtensionBlackListValidator->getErrors())) { + $preparedErrors = []; + $preparedErrors[$field->fieldDefinitionId][$field->languageCode] = $errors; + + throw ContentFieldValidationException::createNewWithMultiline( + $preparedErrors, + $versionInfo->contentInfo->name + ); + } + $field->value->externalData['mimeType'] = $this->mimeTypeDetector->getFromPath($field->value->externalData['inputUri']); $createStruct = $this->ioService->newBinaryCreateStructFromLocalFile($field->value->externalData['inputUri']); $createStruct->id = $this->pathGenerator->getStoragePathForField($field, $versionInfo); diff --git a/src/lib/FieldType/Image/ImageStorage.php b/src/lib/FieldType/Image/ImageStorage.php index 3c8d55ff81..60d1236a73 100644 --- a/src/lib/FieldType/Image/ImageStorage.php +++ b/src/lib/FieldType/Image/ImageStorage.php @@ -10,7 +10,9 @@ use Ibexa\Contracts\Core\FieldType\StorageGatewayInterface; use Ibexa\Contracts\Core\Persistence\Content\Field; use Ibexa\Contracts\Core\Persistence\Content\VersionInfo; +use Ibexa\Core\Base\Exceptions\ContentFieldValidationException; use Ibexa\Core\Base\Exceptions\InvalidArgumentException; +use Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator; use Ibexa\Core\IO\FilePathNormalizerInterface; use Ibexa\Core\IO\IOServiceInterface; use Ibexa\Core\IO\MetadataHandler; @@ -38,13 +40,17 @@ class ImageStorage extends GatewayBasedStorage /** @var \Ibexa\Core\IO\FilePathNormalizerInterface */ protected $filePathNormalizer; + /** @var \Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator */ + protected $fileExtensionBlackListValidator; + public function __construct( StorageGatewayInterface $gateway, IOServiceInterface $ioService, PathGenerator $pathGenerator, MetadataHandler $imageSizeMetadataHandler, AliasCleanerInterface $aliasCleaner, - FilePathNormalizerInterface $filePathNormalizer + FilePathNormalizerInterface $filePathNormalizer, + FileExtensionBlackListValidator $fileExtensionBlackListValidator ) { parent::__construct($gateway); $this->ioService = $ioService; @@ -52,8 +58,14 @@ public function __construct( $this->imageSizeMetadataHandler = $imageSizeMetadataHandler; $this->aliasCleaner = $aliasCleaner; $this->filePathNormalizer = $filePathNormalizer; + $this->fileExtensionBlackListValidator = $fileExtensionBlackListValidator; } + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\ContentFieldValidationException + */ public function storeFieldData(VersionInfo $versionInfo, Field $field, array $context) { $contentMetaData = [ @@ -64,6 +76,17 @@ public function storeFieldData(VersionInfo $versionInfo, Field $field, array $co // new image if (isset($field->value->externalData)) { + $this->fileExtensionBlackListValidator->validateFileExtension($field->value->externalData['fileName']); + if (!empty($errors = $this->fileExtensionBlackListValidator->getErrors())) { + $preparedErrors = []; + $preparedErrors[$field->fieldDefinitionId][$field->languageCode] = $errors; + + throw ContentFieldValidationException::createNewWithMultiline( + $preparedErrors, + $versionInfo->contentInfo->name + ); + } + $targetPath = sprintf( '%s/%s', $this->pathGenerator->getStoragePathForField( diff --git a/src/lib/FieldType/Validator/FileExtensionBlackListValidator.php b/src/lib/FieldType/Validator/FileExtensionBlackListValidator.php index 7599294d32..510d5d2ee9 100644 --- a/src/lib/FieldType/Validator/FileExtensionBlackListValidator.php +++ b/src/lib/FieldType/Validator/FileExtensionBlackListValidator.php @@ -47,24 +47,41 @@ public function validateConstraints($constraints) * {@inheritdoc} */ public function validate(BaseValue $value, ?FieldDefinition $fieldDefinition = null) + { + $this->errors = []; + + $this->validateFileExtension($value->fileName); + + return empty($this->errors); + } + + public function validateFileExtension(string $fileName): void { if ( - pathinfo($value->fileName, PATHINFO_BASENAME) !== $value->fileName || - in_array(strtolower(pathinfo($value->fileName, PATHINFO_EXTENSION)), $this->constraints['extensionsBlackList'], true) + pathinfo($fileName, PATHINFO_BASENAME) !== $fileName + || in_array( + strtolower(pathinfo($fileName, PATHINFO_EXTENSION)), + $this->constraints['extensionsBlackList'], + true + ) ) { $this->errors[] = new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, [ '%extensionsBlackList%' => implode(', ', $this->constraints['extensionsBlackList']), ], 'fileExtensionBlackList' ); - - return false; } + } - return true; + /** + * @return array<\Ibexa\Contracts\Core\FieldType\ValidationError> + */ + public function getErrors(): array + { + return $this->errors; } } diff --git a/src/lib/Resources/settings/fieldtype_external_storages.yml b/src/lib/Resources/settings/fieldtype_external_storages.yml index da1fd0c4fa..d91e8f9144 100644 --- a/src/lib/Resources/settings/fieldtype_external_storages.yml +++ b/src/lib/Resources/settings/fieldtype_external_storages.yml @@ -2,10 +2,11 @@ services: Ibexa\Core\FieldType\BinaryFile\BinaryFileStorage: class: Ibexa\Core\FieldType\BinaryFile\BinaryFileStorage arguments: - - '@Ibexa\Core\FieldType\BinaryFile\BinaryFileStorage\Gateway\DoctrineStorage' - - '@ibexa.field_type.ezbinaryfile.io_service' - - '@Ibexa\Core\FieldType\BinaryBase\PathGenerator\LegacyPathGenerator' - - '@ibexa.core.io.mimeTypeDetector' + $gateway: '@Ibexa\Core\FieldType\BinaryFile\BinaryFileStorage\Gateway\DoctrineStorage' + $ioService: '@ibexa.field_type.ezbinaryfile.io_service' + $pathGenerator: '@Ibexa\Core\FieldType\BinaryBase\PathGenerator\LegacyPathGenerator' + $mimeTypeDetector: '@ibexa.core.io.mimeTypeDetector' + $fileExtensionBlackListValidator: '@Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator' tags: - {name: ibexa.field_type.storage.external.handler, alias: ezbinaryfile} @@ -18,6 +19,7 @@ services: $imageSizeMetadataHandler: '@Ibexa\Core\IO\MetadataHandler\ImageSize' $aliasCleaner: '@Ibexa\Core\FieldType\Image\AliasCleanerInterface' $filePathNormalizer: '@Ibexa\Core\IO\FilePathNormalizerInterface' + $fileExtensionBlackListValidator: '@Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator' tags: - {name: ibexa.field_type.storage.external.handler, alias: ezimage} @@ -30,10 +32,11 @@ services: Ibexa\Core\FieldType\Media\MediaStorage: class: Ibexa\Core\FieldType\Media\MediaStorage arguments: - - '@Ibexa\Core\FieldType\Media\MediaStorage\Gateway\DoctrineStorage' - - '@ibexa.field_type.ezbinaryfile.io_service' - - '@Ibexa\Core\FieldType\BinaryBase\PathGenerator\LegacyPathGenerator' - - '@ibexa.core.io.mimeTypeDetector' + $gateway: '@Ibexa\Core\FieldType\Media\MediaStorage\Gateway\DoctrineStorage' + $ioService: '@ibexa.field_type.ezbinaryfile.io_service' + $pathGenerator: '@Ibexa\Core\FieldType\BinaryBase\PathGenerator\LegacyPathGenerator' + $mimeTypeDetector: '@ibexa.core.io.mimeTypeDetector' + $fileExtensionBlackListValidator: '@Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator' tags: - {name: ibexa.field_type.storage.external.handler, alias: ezmedia} diff --git a/tests/integration/Core/BinaryBase/BinaryBaseStorage/BinaryBaseStorageTest.php b/tests/integration/Core/BinaryBase/BinaryBaseStorage/BinaryBaseStorageTest.php index 3bca25e696..468fcc2562 100644 --- a/tests/integration/Core/BinaryBase/BinaryBaseStorage/BinaryBaseStorageTest.php +++ b/tests/integration/Core/BinaryBase/BinaryBaseStorage/BinaryBaseStorageTest.php @@ -17,6 +17,7 @@ use Ibexa\Core\FieldType\BinaryBase\BinaryBaseStorage; use Ibexa\Core\FieldType\BinaryBase\BinaryBaseStorage\Gateway; use Ibexa\Core\FieldType\BinaryFile\BinaryFileStorage\Gateway\DoctrineStorage; +use Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator; use Ibexa\Core\IO\IOServiceInterface; use Ibexa\Core\IO\Values\BinaryFile; use Ibexa\Core\IO\Values\BinaryFileCreateStruct; @@ -36,6 +37,9 @@ class BinaryBaseStorageTest extends BaseCoreFieldTypeIntegrationTest /** @var \Ibexa\Core\FieldType\BinaryBase\BinaryBaseStorage|\PHPUnit\Framework\MockObject\MockObject */ protected $storage; + /** @var \Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator&\PHPUnit\Framework\MockObject\MockObject */ + protected $fileExtensionBlackListValidatorMock; + protected function setUp(): void { parent::setUp(); @@ -43,6 +47,9 @@ protected function setUp(): void $this->gateway = $this->getStorageGateway(); $this->pathGeneratorMock = $this->createMock(PathGenerator::class); $this->ioServiceMock = $this->createMock(IOServiceInterface::class); + $this->fileExtensionBlackListValidatorMock = $this->createMock( + FileExtensionBlackListValidator::class + ); $this->storage = $this->getMockBuilder(BinaryBaseStorage::class) ->onlyMethods([]) ->setConstructorArgs( @@ -51,6 +58,7 @@ protected function setUp(): void $this->ioServiceMock, $this->pathGeneratorMock, $this->createMock(MimeTypeDetector::class), + $this->fileExtensionBlackListValidatorMock, ] ) ->getMock(); diff --git a/tests/integration/Core/Image/ImageStorage/ImageStorageTest.php b/tests/integration/Core/Image/ImageStorage/ImageStorageTest.php index 33d449d03b..024cbd5de5 100644 --- a/tests/integration/Core/Image/ImageStorage/ImageStorageTest.php +++ b/tests/integration/Core/Image/ImageStorage/ImageStorageTest.php @@ -16,6 +16,7 @@ use Ibexa\Core\FieldType\Image\ImageStorage; use Ibexa\Core\FieldType\Image\ImageStorage\Gateway\DoctrineStorage; use Ibexa\Core\FieldType\Image\PathGenerator; +use Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator; use Ibexa\Core\IO\FilePathNormalizerInterface; use Ibexa\Core\IO\IOServiceInterface; use Ibexa\Core\IO\MetadataHandler; @@ -50,6 +51,9 @@ final class ImageStorageTest extends BaseCoreFieldTypeIntegrationTest /** @var \Ibexa\Core\FieldType\Image\ImageStorage */ private $storage; + /** @var \Ibexa\Core\FieldType\Validator\FileExtensionBlackListValidator&\PHPUnit\Framework\MockObject\MockObject */ + private $fileExtensionBlackListValidator; + protected function setUp(): void { parent::setUp(); @@ -61,6 +65,7 @@ protected function setUp(): void $this->aliasCleaner = $this->createMock(AliasCleanerInterface::class); $this->filePathNormalizer = $this->createMock(FilePathNormalizerInterface::class); $this->ioService = $this->createMock(IOServiceInterface::class); + $this->fileExtensionBlackListValidator = $this->createMock(FileExtensionBlackListValidator::class); $this->storage = new ImageStorage( $this->gateway, $this->ioService, @@ -68,6 +73,7 @@ protected function setUp(): void $this->imageSizeMetadataHandler, $this->aliasCleaner, $this->filePathNormalizer, + $this->fileExtensionBlackListValidator ); } diff --git a/tests/lib/FieldType/BinaryFileTest.php b/tests/lib/FieldType/BinaryFileTest.php index 465b9eb0fc..b8cf9bfcfe 100644 --- a/tests/lib/FieldType/BinaryFileTest.php +++ b/tests/lib/FieldType/BinaryFileTest.php @@ -595,7 +595,7 @@ public function provideInvalidDataForValidate() ), [ new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, ['%extensionsBlackList%' => implode(', ', $this->blackListedExtensions)], 'fileExtensionBlackList' @@ -623,7 +623,7 @@ public function provideInvalidDataForValidate() ), [ new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, ['%extensionsBlackList%' => implode(', ', $this->blackListedExtensions)], 'fileExtensionBlackList' diff --git a/tests/lib/FieldType/ImageTest.php b/tests/lib/FieldType/ImageTest.php index dfa120ec2d..bcf4b96c2e 100644 --- a/tests/lib/FieldType/ImageTest.php +++ b/tests/lib/FieldType/ImageTest.php @@ -706,7 +706,7 @@ public function provideInvalidDataForValidate() ), [ new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, ['%extensionsBlackList%' => implode(', ', $this->blackListedExtensions)], 'fileExtensionBlackList' @@ -738,7 +738,7 @@ public function provideInvalidDataForValidate() ), [ new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, ['%extensionsBlackList%' => implode(', ', $this->blackListedExtensions)], 'fileExtensionBlackList' @@ -773,7 +773,7 @@ public function provideInvalidDataForValidate() ), [ new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, ['%extensionsBlackList%' => implode(', ', $this->blackListedExtensions)], 'fileExtensionBlackList' @@ -805,7 +805,7 @@ public function provideInvalidDataForValidate() ), [ new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, ['%extensionsBlackList%' => implode(', ', $this->blackListedExtensions)], 'fileExtensionBlackList' diff --git a/tests/lib/FieldType/MediaTest.php b/tests/lib/FieldType/MediaTest.php index 196ba7eb9d..d76f2f684e 100644 --- a/tests/lib/FieldType/MediaTest.php +++ b/tests/lib/FieldType/MediaTest.php @@ -761,7 +761,7 @@ public function provideInvalidDataForValidate() ), [ new ValidationError( - 'A valid file is required. Following file extensions are on the blacklist: %extensionsBlackList%', + 'A valid file is required. The following file extensions are not allowed: %extensionsBlackList%', null, ['%extensionsBlackList%' => implode(', ', $this->blackListedExtensions)], 'fileExtensionBlackList'