From f75c32934558896cbf63174042a0b33ae85c62ce Mon Sep 17 00:00:00 2001 From: Bartek Date: Wed, 19 Jan 2022 13:10:52 +0100 Subject: [PATCH] Merge IBX-1392: Implemented image filenames sanitization (#52) --- .../Command/NormalizeImagesPathsCommand.php | 34 ++++++- src/bundle/Core/Resources/config/commands.yml | 1 + src/lib/IO/FilePathNormalizer/Flysystem.php | 20 ++++ src/lib/Resources/settings/io.yml | 5 +- .../FieldType/ImageIntegrationTest.php | 26 +++--- .../_fixtures/1234eeee1234-image.jpg | Bin 0 -> 2836 bytes .../_fixtures/2222eeee1111-image.png | Bin 0 -> 12174 bytes .../IO/FilePathNormalizer/FlysystemTest.php | 87 ++++++++++++++++++ 8 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 tests/integration/Core/Repository/FieldType/_fixtures/1234eeee1234-image.jpg create mode 100644 tests/integration/Core/Repository/FieldType/_fixtures/2222eeee1111-image.png create mode 100644 tests/lib/IO/FilePathNormalizer/FlysystemTest.php diff --git a/src/bundle/Core/Command/NormalizeImagesPathsCommand.php b/src/bundle/Core/Command/NormalizeImagesPathsCommand.php index 4fbeb27899..32e8422cc0 100644 --- a/src/bundle/Core/Command/NormalizeImagesPathsCommand.php +++ b/src/bundle/Core/Command/NormalizeImagesPathsCommand.php @@ -11,6 +11,9 @@ use Doctrine\DBAL\Driver\Connection; use Ibexa\Core\FieldType\Image\ImageStorage\Gateway as ImageStorageGateway; use Ibexa\Core\IO\FilePathNormalizerInterface; +use Ibexa\Core\IO\IOServiceInterface; +use Ibexa\Core\IO\Values\BinaryFile; +use Ibexa\Core\IO\Values\BinaryFileCreateStruct; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -40,16 +43,21 @@ final class NormalizeImagesPathsCommand extends Command /** @var \Doctrine\DBAL\Driver\Connection */ private $connection; + /** @var \Ibexa\Core\IO\IOServiceInterface */ + private $ioService; + public function __construct( ImageStorageGateway $imageGateway, FilePathNormalizerInterface $filePathNormalizer, - Connection $connection + Connection $connection, + IOServiceInterface $ioService ) { parent::__construct(); $this->imageGateway = $imageGateway; $this->filePathNormalizer = $filePathNormalizer; $this->connection = $connection; + $this->ioService = $ioService; } protected function configure() @@ -163,6 +171,30 @@ private function updateImagePath(int $fieldId, string $oldPath, string $newPath) $this->imageGateway->updateImagePath($fieldId, $oldPath, $newPath); } } + + $this->moveFile($oldFileName, $newFilename, $oldPath); + } + + private function moveFile(string $oldFileName, string $newFileName, string $oldPath): void + { + $oldBinaryFile = $this->ioService->loadBinaryFileByUri(\DIRECTORY_SEPARATOR . $oldPath); + $newId = str_replace($oldFileName, $newFileName, $oldBinaryFile->id); + $inputStream = $this->ioService->getFileInputStream($oldBinaryFile); + + $binaryCreateStruct = new BinaryFileCreateStruct( + [ + 'id' => $newId, + 'size' => $oldBinaryFile->size, + 'inputStream' => $inputStream, + 'mimeType' => $this->ioService->getMimeType($oldBinaryFile->id), + ] + ); + + $newBinaryFile = $this->ioService->createBinaryFile($binaryCreateStruct); + + if ($newBinaryFile instanceof BinaryFile) { + $this->ioService->deleteBinaryFile($oldBinaryFile); + } } } diff --git a/src/bundle/Core/Resources/config/commands.yml b/src/bundle/Core/Resources/config/commands.yml index 603a073c3a..8bfe9cf999 100644 --- a/src/bundle/Core/Resources/config/commands.yml +++ b/src/bundle/Core/Resources/config/commands.yml @@ -51,6 +51,7 @@ services: arguments: $connection: '@ezpublish.persistence.connection' $imageGateway: '@ezpublish.fieldType.ezimage.storage_gateway' + $ioService: '@ezpublish.fieldType.ezimage.io_service' tags: - { name: console.command } diff --git a/src/lib/IO/FilePathNormalizer/Flysystem.php b/src/lib/IO/FilePathNormalizer/Flysystem.php index 07b3e04994..a10d54d2cf 100644 --- a/src/lib/IO/FilePathNormalizer/Flysystem.php +++ b/src/lib/IO/FilePathNormalizer/Flysystem.php @@ -9,12 +9,32 @@ namespace Ibexa\Core\IO\FilePathNormalizer; use Ibexa\Core\IO\FilePathNormalizerInterface; +use Ibexa\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter; use League\Flysystem\Util; final class Flysystem implements FilePathNormalizerInterface { + private const HASH_PATTERN = '/^[0-9a-f]{12}-/'; + + /** @var \Ibexa\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter */ + private $slugConverter; + + public function __construct(SlugConverter $slugConverter) + { + $this->slugConverter = $slugConverter; + } + public function normalizePath(string $filePath): string { + $fileName = pathinfo($filePath, PATHINFO_BASENAME); + $directory = pathinfo($filePath, PATHINFO_DIRNAME); + + $fileName = $this->slugConverter->convert($fileName); + + $hash = preg_match(self::HASH_PATTERN, $fileName) ? '' : bin2hex(random_bytes(6)) . '-'; + + $filePath = $directory . \DIRECTORY_SEPARATOR . $hash . $fileName; + return Util::normalizePath($filePath); } } diff --git a/src/lib/Resources/settings/io.yml b/src/lib/Resources/settings/io.yml index 2902b13ef1..973b10d2ae 100644 --- a/src/lib/Resources/settings/io.yml +++ b/src/lib/Resources/settings/io.yml @@ -75,5 +75,8 @@ services: - ~ - "@ezpublish.core.io.image_fieldtype.legacy_url_decorator" - Ibexa\Core\IO\FilePathNormalizer\Flysystem: ~ + Ibexa\Core\IO\FilePathNormalizer\Flysystem: + arguments: + $slugConverter: '@ezpublish.persistence.slug_converter' + Ibexa\Core\IO\FilePathNormalizerInterface: '@Ibexa\Core\IO\FilePathNormalizer\Flysystem' diff --git a/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php b/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php index fef74344f2..cbe6d67976 100644 --- a/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php +++ b/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php @@ -182,12 +182,10 @@ public function getFieldName() * * Asserts that the data provided by {@link getValidCreationFieldData()} * was stored and loaded correctly. - * - * @param \Ibexa\Contracts\Core\Repository\Values\Content\Field $field */ - public function assertFieldDataLoadedCorrect(Field $field) + public function assertFieldDataLoadedCorrect(Field $field): void { - $this->assertInstanceOf( + self::assertInstanceOf( ImageValue::class, $field->value ); @@ -198,12 +196,15 @@ public function assertFieldDataLoadedCorrect(Field $field) // Will be nullified by external storage $expectedData['inputUri'] = null; + // Will be changed by external storage as fileName will be decorated with a hash + $expectedData['fileName'] = $field->value->fileName; + $this->assertPropertiesCorrect( $expectedData, $field->value ); - $this->assertTrue( + self::assertTrue( $this->uriExistsOnIO($field->value->uri), "Asserting that {$field->value->uri} exists." ); @@ -247,7 +248,7 @@ public function getValidUpdateFieldData() */ public function assertUpdatedFieldDataLoadedCorrect(Field $field) { - $this->assertInstanceOf( + self::assertInstanceOf( ImageValue::class, $field->value ); @@ -258,6 +259,9 @@ public function assertUpdatedFieldDataLoadedCorrect(Field $field) // Will change during storage $expectedData['inputUri'] = null; + // Will change during storage as fileName will be decorated with a hash + $expectedData['fileName'] = $field->value->fileName; + $expectedData['uri'] = $field->value->uri; $this->assertPropertiesCorrect( @@ -265,7 +269,7 @@ public function assertUpdatedFieldDataLoadedCorrect(Field $field) $field->value ); - $this->assertTrue( + self::assertTrue( $this->uriExistsOnIO($field->value->uri), "Asserting that file {$field->value->uri} exists" ); @@ -573,8 +577,8 @@ protected function getValidSearchValueOne() { return new ImageValue( [ - 'fileName' => 'cafe-terrace-at-night.jpg', - 'inputUri' => ($path = __DIR__ . '/_fixtures/image.jpg'), + 'fileName' => '1234eeee1234-cafe-terrace-at-night.jpg', + 'inputUri' => ($path = __DIR__ . '/_fixtures/1234eeee1234-image.jpg'), 'alternativeText' => 'café terrace at night, also known as the cafe terrace on the place du forum', 'fileSize' => filesize($path), ] @@ -585,8 +589,8 @@ protected function getValidSearchValueTwo() { return new ImageValue( [ - 'fileName' => 'thatched-cottages-at-cordeville.png', - 'inputUri' => ($path = __DIR__ . '/_fixtures/image.png'), + 'fileName' => '2222eeee1111-thatched-cottages-at-cordeville.png', + 'inputUri' => ($path = __DIR__ . '/_fixtures/2222eeee1111-image.png'), 'alternativeText' => 'chaumes de cordeville à auvers-sur-oise', 'fileSize' => filesize($path), ] diff --git a/tests/integration/Core/Repository/FieldType/_fixtures/1234eeee1234-image.jpg b/tests/integration/Core/Repository/FieldType/_fixtures/1234eeee1234-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ddd8409238c4bdaff37b4fec29f9a9706cdccd6 GIT binary patch literal 2836 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 literal 0 HcmV?d00001 diff --git a/tests/integration/Core/Repository/FieldType/_fixtures/2222eeee1111-image.png b/tests/integration/Core/Repository/FieldType/_fixtures/2222eeee1111-image.png new file mode 100644 index 0000000000000000000000000000000000000000..b0794c3795b31e6127eee47001c9e30a0148255b GIT binary patch literal 12174 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 = $this->createMock(SlugConverter::class); + $this->filePathNormalizer = new Flysystem($this->slugConverter); + } + + /** + * @dataProvider providerForTestNormalizePath + */ + public function testNormalizePath( + string $originalPath, + string $fileName, + string $sluggedFileName, + string $regex + ): void { + $this->slugConverter + ->expects(self::once()) + ->method('convert') + ->with($fileName) + ->willReturn($sluggedFileName); + + $normalizedPath = $this->filePathNormalizer->normalizePath($originalPath); + + self::assertStringEndsWith($sluggedFileName, $normalizedPath); + self::assertRegExp($regex, $normalizedPath); + } + + public function providerForTestNormalizePath(): array + { + $defaultPattern = '/\/[0-9a-f]{12}-'; + + return [ + 'No special chars' => [ + '4/3/2/234/1/image.jpg', + 'image.jpg', + 'image.jpg', + $defaultPattern . 'image.jpg/', + ], + 'Spaces in the filename' => [ + '4/3/2/234/1/image with spaces.jpg', + 'image with spaces.jpg', + 'image-with-spaces.jpg', + $defaultPattern . 'image-with-spaces.jpg/', + ], + 'Encoded spaces in the name' => [ + '4/3/2/234/1/image%20+no+spaces.jpg', + 'image%20+no+spaces.jpg', + 'image-20-nospaces.jpg', + $defaultPattern . 'image-20-nospaces.jpg/', + ], + 'Special chars in the name' => [ + '4/3/2/234/1/image%20+no+spaces?.jpg', + 'image%20+no+spaces?.jpg', + 'image-20-nospaces.jpg', + $defaultPattern . 'image-20-nospaces.jpg/', + ], + 'Already hashed name' => [ + '4/3/2/234/1/14ff44718877-hashed.jpg', + '14ff44718877-hashed.jpg', + '14ff44718877-hashed.jpg', + '/^4\/3\/2\/234\/1\/14ff44718877-hashed.jpg$/', + ], + ]; + } +}