From 04451df2acbdfeee148d5610a50b117e2fd0a8f1 Mon Sep 17 00:00:00 2001 From: Bartek Date: Tue, 18 Jan 2022 10:51:31 +0100 Subject: [PATCH] Merge pull request from GHSA-44m4-9cjp-j587 --- .../Command/NormalizeImagesPathsCommand.php | 34 +++++++- .../Resources/config/commands.yml | 1 + .../Tests/FieldType/ImageIntegrationTest.php | 22 ++--- .../Core/IO/FilePathNormalizer/Flysystem.php | 20 +++++ .../FilePathNormalizer/FlysystemTest.php | 87 +++++++++++++++++++ eZ/Publish/Core/settings/io.yml | 5 +- 6 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 eZ/Publish/Core/IO/Tests/FilePathNormalizer/FlysystemTest.php diff --git a/eZ/Bundle/EzPublishCoreBundle/Command/NormalizeImagesPathsCommand.php b/eZ/Bundle/EzPublishCoreBundle/Command/NormalizeImagesPathsCommand.php index 30423b756c5..3a86c5ceb9e 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Command/NormalizeImagesPathsCommand.php +++ b/eZ/Bundle/EzPublishCoreBundle/Command/NormalizeImagesPathsCommand.php @@ -11,6 +11,9 @@ use Doctrine\DBAL\Driver\Connection; use eZ\Publish\Core\FieldType\Image\ImageStorage\Gateway as ImageStorageGateway; use eZ\Publish\Core\IO\FilePathNormalizerInterface; +use eZ\Publish\Core\IO\IOServiceInterface; +use eZ\Publish\Core\IO\Values\BinaryFile; +use eZ\Publish\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 \eZ\Publish\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,5 +171,29 @@ 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/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml index 39f911db61c..863b6f9a840 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/commands.yml @@ -27,3 +27,4 @@ services: autoconfigure: true arguments: $connection: '@ezpublish.persistence.connection' + $ioService: '@ezpublish.fieldType.ezimage.io_service' diff --git a/eZ/Publish/API/Repository/Tests/FieldType/ImageIntegrationTest.php b/eZ/Publish/API/Repository/Tests/FieldType/ImageIntegrationTest.php index 049fe57e08e..0a50ec2586e 100644 --- a/eZ/Publish/API/Repository/Tests/FieldType/ImageIntegrationTest.php +++ b/eZ/Publish/API/Repository/Tests/FieldType/ImageIntegrationTest.php @@ -181,12 +181,10 @@ public function getFieldName() * * Asserts that the data provided by {@link getValidCreationFieldData()} * was stored and loaded correctly. - * - * @param Field $field */ - public function assertFieldDataLoadedCorrect(Field $field) + public function assertFieldDataLoadedCorrect(Field $field): void { - $this->assertInstanceOf( + self::assertInstanceOf( 'eZ\\Publish\\Core\\FieldType\\Image\\Value', $field->value ); @@ -197,12 +195,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." ); @@ -262,12 +263,10 @@ public function getValidUpdateFieldData() * Get externals updated field data values. * * This is a PHPUnit data provider - * - * @return array */ - public function assertUpdatedFieldDataLoadedCorrect(Field $field) + public function assertUpdatedFieldDataLoadedCorrect(Field $field): void { - $this->assertInstanceOf( + self::assertInstanceOf( 'eZ\\Publish\\Core\\FieldType\\Image\\Value', $field->value ); @@ -278,6 +277,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( @@ -285,7 +287,7 @@ public function assertUpdatedFieldDataLoadedCorrect(Field $field) $field->value ); - $this->assertTrue( + self::assertTrue( $this->uriExistsOnIO($field->value->uri), "Asserting that file {$field->value->uri} exists" ); diff --git a/eZ/Publish/Core/IO/FilePathNormalizer/Flysystem.php b/eZ/Publish/Core/IO/FilePathNormalizer/Flysystem.php index 980bd1412f1..3d53fc70de2 100644 --- a/eZ/Publish/Core/IO/FilePathNormalizer/Flysystem.php +++ b/eZ/Publish/Core/IO/FilePathNormalizer/Flysystem.php @@ -9,12 +9,32 @@ namespace eZ\Publish\Core\IO\FilePathNormalizer; use eZ\Publish\Core\IO\FilePathNormalizerInterface; +use eZ\Publish\Core\Persistence\Legacy\Content\UrlAlias\SlugConverter; use League\Flysystem\Util; final class Flysystem implements FilePathNormalizerInterface { + private const HASH_PATTERN = '/^[0-9a-f]{12}-/'; + + /** @var \eZ\Publish\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/eZ/Publish/Core/IO/Tests/FilePathNormalizer/FlysystemTest.php b/eZ/Publish/Core/IO/Tests/FilePathNormalizer/FlysystemTest.php new file mode 100644 index 00000000000..f79eec14df1 --- /dev/null +++ b/eZ/Publish/Core/IO/Tests/FilePathNormalizer/FlysystemTest.php @@ -0,0 +1,87 @@ +slugConverter = $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$/', + ], + ]; + } +} diff --git a/eZ/Publish/Core/settings/io.yml b/eZ/Publish/Core/settings/io.yml index e9d579be34b..d84e9bca3fd 100644 --- a/eZ/Publish/Core/settings/io.yml +++ b/eZ/Publish/Core/settings/io.yml @@ -86,5 +86,8 @@ services: - ~ - "@ezpublish.core.io.image_fieldtype.legacy_url_decorator" - eZ\Publish\Core\IO\FilePathNormalizer\Flysystem: ~ + eZ\Publish\Core\IO\FilePathNormalizer\Flysystem: + arguments: + $slugConverter: '@ezpublish.persistence.slug_converter' + eZ\Publish\Core\IO\FilePathNormalizerInterface: '@eZ\Publish\Core\IO\FilePathNormalizer\Flysystem'