From e88368cf209252cf99b904a771d5d1fa328df2a8 Mon Sep 17 00:00:00 2001 From: ciastektk Date: Mon, 20 Nov 2023 14:45:14 +0100 Subject: [PATCH] IBX-6620: Added image search criterions (#284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dawid ParafiƄski --- .github/workflows/ci.yaml | 8 +- phpunit-integration-legacy-solr.xml | 4 +- phpunit-integration-legacy.xml | 1 + .../Values/Content/Query/Criterion/Image.php | 102 +++++ .../Image/AbstractImageCompositeCriterion.php | 108 +++++ .../Image/AbstractImageRangeCriterion.php | 93 +++++ .../Query/Criterion/Image/Dimensions.php | 69 ++++ .../Query/Criterion/Image/FileSize.php | 35 ++ .../Content/Query/Criterion/Image/Height.php | 13 + .../Query/Criterion/Image/MimeType.php | 42 ++ .../Query/Criterion/Image/Orientation.php | 100 +++++ .../Content/Query/Criterion/Image/Width.php | 13 + src/lib/FieldType/Image/Orientation.php | 16 + src/lib/FieldType/Image/SearchField.php | 38 ++ src/lib/FieldType/Image/Type.php | 7 + src/lib/FieldType/Image/Value.php | 2 + .../FieldValue/Converter/ImageConverter.php | 2 +- .../FieldType/ImageIntegrationTest.php | 13 + .../Repository/SearchServiceImageTest.php | 383 ++++++++++++++++++ .../Repository/_fixtures/image/landscape.jpg | Bin 0 -> 3461 bytes .../Repository/_fixtures/image/portrait.jpg | Bin 0 -> 2836 bytes .../Repository/_fixtures/image/square.png | Bin 0 -> 177 bytes .../Core/RepositorySearchTestCase.php | 25 ++ tests/lib/FieldType/ImageTest.php | 4 + 24 files changed, 1072 insertions(+), 6 deletions(-) create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageCompositeCriterion.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageRangeCriterion.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/Dimensions.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/FileSize.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/Height.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/MimeType.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/Orientation.php create mode 100644 src/contracts/Repository/Values/Content/Query/Criterion/Image/Width.php create mode 100644 src/lib/FieldType/Image/Orientation.php create mode 100644 tests/integration/Core/Repository/SearchServiceImageTest.php create mode 100644 tests/integration/Core/Repository/_fixtures/image/landscape.jpg create mode 100644 tests/integration/Core/Repository/_fixtures/image/portrait.jpg create mode 100644 tests/integration/Core/Repository/_fixtures/image/square.png create mode 100644 tests/integration/Core/RepositorySearchTestCase.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3d110a576e..47d22c7d31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -192,7 +192,7 @@ jobs: image: redis ports: - 6379:6379 - options: + options: --memory=60m solr: image: ghcr.io/ibexa/core/solr @@ -210,7 +210,7 @@ jobs: - '7.4' - '8.0' - '8.1' - steps: + steps: - uses: actions/checkout@v2 with: fetch-depth: 0 @@ -220,10 +220,10 @@ jobs: with: php-version: ${{ matrix.php }} coverage: none - + - name: Add solr dependency run: | - VERSION=$(jq -r '.extra | ."branch-alias" | ."dev-main"' < composer.json) + VERSION="dev-ibx-6620-added-image-criterion-visitors-for-ci as 4.6.x-dev" composer require --no-update "ibexa/solr:$VERSION" - uses: "ramsey/composer-install@v1" diff --git a/phpunit-integration-legacy-solr.xml b/phpunit-integration-legacy-solr.xml index 3dce726268..8c5db18b42 100644 --- a/phpunit-integration-legacy-solr.xml +++ b/phpunit-integration-legacy-solr.xml @@ -18,7 +18,9 @@ - + + + diff --git a/phpunit-integration-legacy.xml b/phpunit-integration-legacy.xml index 6b1da1c84c..cb4a636435 100644 --- a/phpunit-integration-legacy.xml +++ b/phpunit-integration-legacy.xml @@ -18,6 +18,7 @@ + diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image.php new file mode 100644 index 0000000000..5dc965b06f --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image.php @@ -0,0 +1,102 @@ +, + * size?: Range, + * width?: Range, + * height?: Range, + * orientation?: string|array, + * } + * + * @template-extends \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Image\AbstractImageCompositeCriterion + */ +final class Image extends Criterion\Image\AbstractImageCompositeCriterion +{ + public const IMAGE_SEARCH_CRITERIA = [ + 'mimeTypes', + 'size', + 'width', + 'height', + 'orientation', + ]; + + protected function getSupportedCriteria(): array + { + return self::IMAGE_SEARCH_CRITERIA; + } + + /** + * @phpstan-param ImageCriteria $data + * + * @return array<\Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion> + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + protected function buildCriteria( + string $fieldDefIdentifier, + array $data + ): array { + $criteria = []; + + if (isset($data['mimeTypes'])) { + $criteria[] = new MimeType( + $fieldDefIdentifier, + $data['mimeTypes'] + ); + } + + if (isset($data['size'])) { + $size = $data['size']; + $criteria[] = new FileSize( + $fieldDefIdentifier, + $this->getMinValue($size), + $this->getMaxValue($size), + ); + } + + if (isset($data['width'])) { + $width = $data['width']; + $criteria[] = new Width( + $fieldDefIdentifier, + $this->getMinValue($width), + $this->getMaxValue($width) + ); + } + + if (isset($data['height'])) { + $height = $data['height']; + $criteria[] = new Height( + $fieldDefIdentifier, + $this->getMinValue($height), + $this->getMaxValue($height) + ); + } + + if (isset($data['orientation'])) { + $criteria[] = new Orientation( + $fieldDefIdentifier, + $data['orientation'] + ); + } + + return $criteria; + } +} diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageCompositeCriterion.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageCompositeCriterion.php new file mode 100644 index 0000000000..f3b43d1e99 --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageCompositeCriterion.php @@ -0,0 +1,108 @@ +validate($data, $this->getSupportedCriteria()); + + $criteria = new Criterion\LogicalAnd( + $this->buildCriteria($fieldDefIdentifier, $data) + ); + + parent::__construct($criteria); + } + + /** + * @phpstan-param TImageCriteria $data + * + * @return array<\Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion> + */ + abstract protected function buildCriteria(string $fieldDefIdentifier, array $data): array; + + /** + * @return array + */ + abstract protected function getSupportedCriteria(): array; + + /** + * @phpstan-param TImageCriteria $data + * + * @param array $supportedCriteria + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + protected function validate( + array $data, + array $supportedCriteria + ): void { + if (empty($data)) { + throw new InvalidArgumentException( + '$data', + sprintf( + 'At least one of the supported criteria should be passed: "%s"', + implode(', ', $supportedCriteria) + ) + ); + } + + $notSupportedCriteria = array_diff( + array_keys($data), + $supportedCriteria + ); + + if (!empty($notSupportedCriteria)) { + throw new InvalidArgumentException( + '$data', + sprintf( + 'Given criteria are not supported: "%s". Supported image criteria: "%s"', + implode(', ', $notSupportedCriteria), + implode(', ', $supportedCriteria) + ) + ); + } + } + + /** + * @param array{min?: int|null} $data + */ + protected function getMinValue(array $data): int + { + return $data['min'] ?? 0; + } + + /** + * @param array{max?: int|null} $data + */ + protected function getMaxValue(array $data): ?int + { + return $data['max'] ?? null; + } +} diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageRangeCriterion.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageRangeCriterion.php new file mode 100644 index 0000000000..31bc7c6c3d --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image/AbstractImageRangeCriterion.php @@ -0,0 +1,93 @@ +validate($minValue, $maxValue); + + $value[] = $minValue; + $operator = Operator::GTE; + + if ($maxValue >= 1) { + $operator = Operator::BETWEEN; + $value[] = $maxValue; + } + + parent::__construct( + $fieldDefIdentifier, + $operator, + $value + ); + } + + public function getSpecifications(): array + { + return [ + new Specifications( + Operator::BETWEEN, + Specifications::FORMAT_ARRAY, + Specifications::TYPE_INTEGER + ), + new Specifications( + Operator::GTE, + Specifications::FORMAT_ARRAY, + Specifications::TYPE_INTEGER + ), + ]; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + protected function validate( + int $minValue, + ?int $maxValue + ): void { + if ($minValue < 0) { + throw new InvalidArgumentException( + '$minValue', + 'Value should be grater or equal 0' + ); + } + + if ( + null !== $maxValue + && $maxValue < 1 + ) { + throw new InvalidArgumentException( + '$maxValue', + 'Value should be grater or equal 1' + ); + } + + if ( + null !== $maxValue + && $minValue > $maxValue + ) { + throw new InvalidArgumentException( + '$minValue', + 'Value should be grater than' . $maxValue + ); + } + } +} diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image/Dimensions.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Dimensions.php new file mode 100644 index 0000000000..d8cc57cf0c --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Dimensions.php @@ -0,0 +1,69 @@ + + */ +final class Dimensions extends AbstractImageCompositeCriterion +{ + public const IMAGE_DIMENSIONS_CRITERIA = [ + 'width', + 'height', + ]; + + /** + * @return array + */ + protected function getSupportedCriteria(): array + { + return self::IMAGE_DIMENSIONS_CRITERIA; + } + + /** + * @phpstan-param ImageCriteria $data + * + * @return array<\Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion> + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + protected function buildCriteria( + string $fieldDefIdentifier, + array $data + ): array { + $criteria = []; + + if (isset($data['width'])) { + $width = $data['width']; + $criteria[] = new Width( + $fieldDefIdentifier, + $this->getMinValue($width), + $this->getMaxValue($width) + ); + } + + if (isset($data['height'])) { + $height = $data['height']; + $criteria[] = new Height( + $fieldDefIdentifier, + $this->getMinValue($height), + $this->getMaxValue($height) + ); + } + + return $criteria; + } +} diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image/FileSize.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image/FileSize.php new file mode 100644 index 0000000000..63fc9f64d9 --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image/FileSize.php @@ -0,0 +1,35 @@ + 0) { + $minFileSize *= 1024 * 1024; + } + + if ($maxFileSize > 0) { + $maxFileSize *= 1024 * 1024; + } + + parent::__construct( + $fieldDefIdentifier, + $minFileSize, + $maxFileSize + ); + } +} diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image/Height.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Height.php new file mode 100644 index 0000000000..78719e5d6e --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Height.php @@ -0,0 +1,13 @@ + $type + */ + public function __construct( + string $fieldDefIdentifier, + $type + ) { + parent::__construct($fieldDefIdentifier, null, $type); + } + + public function getSpecifications(): array + { + return [ + new Specifications( + Operator::EQ, + Specifications::FORMAT_SINGLE, + Specifications::TYPE_STRING + ), + new Specifications( + Operator::IN, + Specifications::FORMAT_ARRAY, + Specifications::TYPE_STRING + ), + ]; + } +} diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image/Orientation.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Orientation.php new file mode 100644 index 0000000000..5bae6e19e2 --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Orientation.php @@ -0,0 +1,100 @@ + $orientation + * + * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException + */ + public function __construct( + string $fieldDefIdentifier, + $orientation + ) { + $this->validate($orientation); + + parent::__construct($fieldDefIdentifier, null, $orientation); + } + + public function getSpecifications(): array + { + return [ + new Specifications( + Operator::EQ, + Specifications::FORMAT_SINGLE, + Specifications::TYPE_STRING + ), + new Specifications( + Operator::IN, + Specifications::FORMAT_ARRAY, + Specifications::TYPE_STRING + ), + ]; + } + + /** + * @param string|array $orientation + * + * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException + */ + private function validate($orientation): void + { + if ( + is_string($orientation) + && !$this->isSupportedOrientation($orientation) + ) { + $this->throwException($orientation); + } + + if (is_array($orientation)) { + $invalidOrientations = array_filter( + $orientation, + fn ($value): bool => !$this->isSupportedOrientation($value) + ); + + if (!empty($invalidOrientations)) { + $this->throwException(implode(', ', $invalidOrientations)); + } + } + } + + private function isSupportedOrientation(string $orientation): bool + { + return in_array($orientation, self::ALLOWED_ORIENTATIONS, true); + } + + /** + * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException + */ + private function throwException(string $whatIsWrong): void + { + throw new InvalidArgumentException( + '$orientation', + sprintf( + 'Invalid image orientation: "%s". Allowed orientations: %s', + $whatIsWrong, + implode(', ', self::ALLOWED_ORIENTATIONS) + ) + ); + } +} diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/Image/Width.php b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Width.php new file mode 100644 index 0000000000..4a5293a5e1 --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/Image/Width.php @@ -0,0 +1,13 @@ +value->data['width'] ?? null; + $height = $field->value->data['height'] ?? null; + return [ new Search\Field( 'filename', @@ -39,6 +42,21 @@ public function getIndexData(Field $field, FieldDefinition $fieldDefinition) $field->value->data['mime'] ?? null, new Search\FieldType\StringField() ), + new Search\Field( + 'width', + $width, + new Search\FieldType\IntegerField() + ), + new Search\Field( + 'height', + $height, + new Search\FieldType\IntegerField() + ), + new Search\Field( + 'orientation', + $this->getOrientation($width, $height), + new Search\FieldType\StringField() + ), ]; } @@ -49,6 +67,9 @@ public function getIndexDefinition() 'alternative_text' => new Search\FieldType\StringField(), 'file_size' => new Search\FieldType\IntegerField(), 'mime_type' => new Search\FieldType\StringField(), + 'width' => new Search\FieldType\IntegerField(), + 'height' => new Search\FieldType\IntegerField(), + 'orientation' => new Search\FieldType\StringField(), ]; } @@ -79,6 +100,23 @@ public function getDefaultSortField() { return $this->getDefaultMatchField(); } + + private function getOrientation( + ?int $width, + ?int $height + ): ?string { + if (null === $width || null === $height) { + return null; + } + + if ($width === $height) { + return Orientation::SQUARE; + } + + return $width > $height + ? Orientation::LANDSCAPE + : Orientation::PORTRAIT; + } } class_alias(SearchField::class, 'eZ\Publish\Core\FieldType\Image\SearchField'); diff --git a/src/lib/FieldType/Image/Type.php b/src/lib/FieldType/Image/Type.php index 76bf759125..ef84995998 100644 --- a/src/lib/FieldType/Image/Type.php +++ b/src/lib/FieldType/Image/Type.php @@ -323,6 +323,7 @@ public function toHash(SPIValue $value) 'width' => $value->width, 'height' => $value->height, 'additionalData' => $value->additionalData, + 'mime' => $value->mime, ]; } @@ -387,6 +388,7 @@ public function fromPersistenceValue(FieldValue $fieldValue) ? $fieldValue->data['height'] : null), 'additionalData' => $fieldValue->data['additionalData'] ?? [], + 'mime' => $fieldValue->data['mime'] ?? null, ] ); @@ -409,6 +411,11 @@ public static function getTranslationMessages(): array Message::create('ezimage.name', 'ibexa_fieldtypes')->setDesc('Image'), ]; } + + public function isSearchable(): bool + { + return true; + } } class_alias(Type::class, 'eZ\Publish\Core\FieldType\Image\Type'); diff --git a/src/lib/FieldType/Image/Value.php b/src/lib/FieldType/Image/Value.php index 4ca142d03b..f8cf5645df 100644 --- a/src/lib/FieldType/Image/Value.php +++ b/src/lib/FieldType/Image/Value.php @@ -93,6 +93,8 @@ class Value extends BaseValue /** @var string[] */ public $additionalData = []; + public ?string $mime = null; + /** * Construct a new Value object. */ diff --git a/src/lib/Persistence/Legacy/Content/FieldValue/Converter/ImageConverter.php b/src/lib/Persistence/Legacy/Content/FieldValue/Converter/ImageConverter.php index 80989af520..0b06c09c40 100644 --- a/src/lib/Persistence/Legacy/Content/FieldValue/Converter/ImageConverter.php +++ b/src/lib/Persistence/Legacy/Content/FieldValue/Converter/ImageConverter.php @@ -137,7 +137,7 @@ protected function fillXml($imageData, $pathInfo, $timestamp) htmlspecialchars($imageData['mime']), // mime_type htmlspecialchars($imageData['width']), // width htmlspecialchars($imageData['height']), // height - htmlspecialchars($imageData['alternativeText']), // alternative_text + htmlspecialchars($imageData['alternativeText'] ?? ''), // alternative_text htmlspecialchars(1293033771), // alias_key, fixed for the original image htmlspecialchars($timestamp), // timestamp // diff --git a/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php b/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php index 8fa2e3e74e..ff9555acc0 100644 --- a/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php +++ b/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php @@ -9,6 +9,7 @@ use Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException; use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Field; +use Ibexa\Contracts\Core\Test\Repository\SetupFactory\Legacy; use Ibexa\Core\FieldType\Image\Value as ImageValue; /** @@ -341,6 +342,7 @@ public function provideToHashData() 'width' => null, 'height' => null, 'additionalData' => [], + 'mime' => null, ], ], [ @@ -354,6 +356,7 @@ public function provideToHashData() 'uri' => "/$path", 'width' => 123, 'height' => 456, + 'mime' => 'image/png', ] ), [ @@ -368,6 +371,7 @@ public function provideToHashData() 'width' => 123, 'height' => 456, 'additionalData' => [], + 'mime' => 'image/png', ], ], ]; @@ -573,6 +577,15 @@ public function testUpdateImageAltTextOnly(): void ); } + protected function checkSearchEngineSupport(): void + { + if ($this->getSetupFactory() instanceof Legacy) { + $this->markTestSkipped( + "'ezimage' field type is not searchable with Legacy Search Engine" + ); + } + } + protected function getValidSearchValueOne() { return new ImageValue( diff --git a/tests/integration/Core/Repository/SearchServiceImageTest.php b/tests/integration/Core/Repository/SearchServiceImageTest.php new file mode 100644 index 0000000000..b0586a14ab --- /dev/null +++ b/tests/integration/Core/Repository/SearchServiceImageTest.php @@ -0,0 +1,383 @@ +createImages(); + + $this->refreshSearch(); + } + + /** + * @dataProvider provideDataForTestCriterion + * @dataProvider provideInvalidDataForTestCriterion + */ + public function testCriterion( + int $expectedCount, + Query\Criterion $imageCriterion + ): void { + if (getenv('SEARCH_ENGINE') === 'legacy') { + self::markTestSkipped('Image criteria are not supported in Legacy Search Engine'); + } + + $query = new Query(); + $query->filter = new Query\Criterion\LogicalAnd( + [ + new Query\Criterion\ContentTypeIdentifier(self::IMAGE_CONTENT_TYPE), + $imageCriterion, + ] + ); + + $searchHits = self::getSearchService()->findContent($query); + + self::assertSame( + $expectedCount, + $searchHits->totalCount + ); + } + + /** + * @return iterable + */ + public function provideDataForTestCriterion(): iterable + { + yield 'Dimensions' => [ + 3, + $this->createDimensionsCriterion( + 0, + 100, + 0, + 100 + ), + ]; + + yield 'FileSize - default values min 0 and max 1' => [ + 3, + $this->createFileSizeCriterion(), + ]; + + yield 'FileSize' => [ + 3, + $this->createFileSizeCriterion(0, 2), + ]; + + yield 'Width' => [ + 3, + $this->createWidthCriterion(0, 100), + ]; + + yield 'Height' => [ + 3, + $this->createHeightCriterion(0, 100), + ]; + + yield 'MimeType - single' => [ + 2, + $this->createMimeTypeCriterion('image/jpeg'), + ]; + + yield 'MimeType - multiple' => [ + 3, + $this->createMimeTypeCriterion( + [ + 'image/jpeg', + 'image/png', + ], + ), + ]; + + yield 'Orientation - landscape' => [ + 1, + $this->createOrientationCriterion(Orientation::LANDSCAPE), + ]; + + yield 'Orientation - portrait' => [ + 1, + $this->createOrientationCriterion(Orientation::PORTRAIT), + ]; + + yield 'Orientation - square' => [ + 1, + $this->createOrientationCriterion(Orientation::SQUARE), + ]; + + yield 'Orientation - multiple' => [ + 3, + $this->createOrientationCriterion( + [ + Orientation::LANDSCAPE, + Orientation::PORTRAIT, + Orientation::SQUARE, + ] + ), + ]; + + yield 'Image' => [ + 2, + new Query\Criterion\Image( + self::IMAGE_FIELD_DEF_IDENTIFIER, + [ + 'mimeTypes' => [ + 'image/jpeg', + 'image/png', + ], + 'size' => [ + 'min' => 0, + 'max' => 1, + ], + 'width' => [ + 'min' => 0, + 'max' => 100, + ], + 'height' => [ + 'min' => 0, + 'max' => 100, + ], + 'orientation' => [ + Orientation::LANDSCAPE, + Orientation::PORTRAIT, + ], + ] + ), + ]; + } + + /** + * @return iterable + */ + public function provideInvalidDataForTestCriterion(): iterable + { + yield 'Dimensions - width and height values too large' => [ + 0, + $this->createDimensionsCriterion( + 101, + 200, + 101, + 300 + ), + ]; + + yield 'FileSize - size value too large' => [ + 0, + $this->createFileSizeCriterion( + 1, + 2 + ), + ]; + + yield 'Width - width value to large' => [ + 0, + $this->createWidthCriterion(101, 200), + ]; + + yield 'Height - height value to large' => [ + 0, + $this->createHeightCriterion(101, 300), + ]; + + yield 'MimeType - invalid single mime type' => [ + 0, + $this->createMimeTypeCriterion('image/invalid'), + ]; + + yield 'MimeType - invalid multiple mime types' => [ + 0, + $this->createMimeTypeCriterion( + [ + 'image/invalid', + 'image/gif', + ] + ), + ]; + } + + /** + * @param string|array $value + */ + private function createMimeTypeCriterion($value): Query\Criterion\Image\MimeType + { + return new Query\Criterion\Image\MimeType( + self::IMAGE_FIELD_DEF_IDENTIFIER, + $value + ); + } + + private function createFileSizeCriterion( + int $min = 0, + ?int $max = null + ): Query\Criterion\Image\FileSize { + return new Query\Criterion\Image\FileSize( + self::IMAGE_FIELD_DEF_IDENTIFIER, + $min, + $max + ); + } + + private function createWidthCriterion( + int $min = 0, + ?int $max = null + ): Query\Criterion\Image\Width { + return new Query\Criterion\Image\Width( + self::IMAGE_FIELD_DEF_IDENTIFIER, + $min, + $max + ); + } + + private function createHeightCriterion( + int $min = 0, + ?int $max = null + ): Query\Criterion\Image\Height { + return new Query\Criterion\Image\Height( + self::IMAGE_FIELD_DEF_IDENTIFIER, + $min, + $max + ); + } + + private function createDimensionsCriterion( + int $minWidth, + int $maxWidth, + int $minHeight, + int $maxHeight + ): Query\Criterion\Image\Dimensions { + return new Query\Criterion\Image\Dimensions( + self::IMAGE_FIELD_DEF_IDENTIFIER, + [ + 'width' => [ + 'min' => $minWidth, + 'max' => $maxWidth, + ], + 'height' => [ + 'min' => $minHeight, + 'max' => $maxHeight, + ], + ] + ); + } + + /** + * @param string|array $value + */ + private function createOrientationCriterion($value): Query\Criterion\Image\Orientation + { + return new Query\Criterion\Image\Orientation( + self::IMAGE_FIELD_DEF_IDENTIFIER, + $value + ); + } + + private function createImages(): void + { + $contentType = $this->loadContentTypeImage(); + foreach (self::IMAGE_FILES as $image) { + $this->createContentImage( + $contentType, + self::IMAGE_FIXTURES_DIR_PATH . $image, + $image + ); + } + } + + private function createContentImage( + ContentType $contentType, + string $path, + string $fileName + ): void { + $contentCreateStruct = self::getContentService()->newContentCreateStruct( + $contentType, + 'eng-GB' + ); + + $imageValue = new ImageValue(); + $imageValue->fileName = $fileName; + $imageValue->path = $path; + + $contentCreateStruct->setField('name', new TextValue('Image'), 'eng-GB'); + $contentCreateStruct->setField('image', $imageValue, 'eng-GB'); + + $contentService = self::getContentService(); + $contentService->publishVersion( + $contentService + ->createContent($contentCreateStruct) + ->getVersionInfo() + ); + } + + private function loadContentTypeImage(): ContentType + { + $imageContentType = self::getContentTypeService()->loadContentTypeByIdentifier(self::IMAGE_CONTENT_TYPE); + + $this->ensureImageFieldTypeIsSearchable($imageContentType); + + return $imageContentType; + } + + private function ensureImageFieldTypeIsSearchable(ContentType $contentType): void + { + $fieldDefinition = $contentType->getFieldDefinition(self::IMAGE_FIELD_DEF_IDENTIFIER); + if ( + null === $fieldDefinition + || $fieldDefinition->isSearchable + ) { + return; + } + + $this->setFieldTypeAsSearchable( + self::getContentTypeService()->createContentTypeDraft($contentType), + $fieldDefinition + ); + } + + private function setFieldTypeAsSearchable( + ContentTypeDraft $contentTypeDraft, + FieldDefinition $fieldDefinition + ): void { + $contentTypeService = self::getContentTypeService(); + $fieldDefinitionUpdateStruct = $contentTypeService->newFieldDefinitionUpdateStruct(); + $fieldDefinitionUpdateStruct->isSearchable = true; + + $contentTypeService = self::getContentTypeService(); + $contentTypeService->updateFieldDefinition( + $contentTypeDraft, + $fieldDefinition, + $fieldDefinitionUpdateStruct + ); + } +} diff --git a/tests/integration/Core/Repository/_fixtures/image/landscape.jpg b/tests/integration/Core/Repository/_fixtures/image/landscape.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1be0750b3b4f52f3718b62bfc12d268d304a2df6 GIT binary patch literal 3461 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 literal 0 HcmV?d00001 diff --git a/tests/integration/Core/Repository/_fixtures/image/square.png b/tests/integration/Core/Repository/_fixtures/image/square.png new file mode 100644 index 0000000000000000000000000000000000000000..159ab778de1f86e74ab1a229980f29604d36625e GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy#^NA%Cx&(BWL^R}oCO|{#S9GG z!XV7ZFl&wkP>{XE)7O>#4hNf{gmU9*r&gelWQl7;iF1B#Zfaf$gL6@8Vo7R>LV0FM zhJw4NZ$Nk>pEyvFoTrOph{fsTloget('ibexa.spi.search'); + if ( + class_exists(SolrHandler::class) + && $handler instanceof SolrHandler + ) { + $handler->commit(); + } + } +} diff --git a/tests/lib/FieldType/ImageTest.php b/tests/lib/FieldType/ImageTest.php index f89f425011..126639d1e1 100644 --- a/tests/lib/FieldType/ImageTest.php +++ b/tests/lib/FieldType/ImageTest.php @@ -336,6 +336,7 @@ public function provideInputForToHash() 'uri' => 'http://' . $this->getImageInputPath(), 'width' => 123, 'height' => 456, + 'mime' => 'image/jpeg', ] ), [ @@ -350,6 +351,7 @@ public function provideInputForToHash() 'width' => 123, 'height' => 456, 'additionalData' => [], + 'mime' => 'image/jpeg', ], ], // BC with 5.0 (EZP-20948). Path can be used as input instead of $inputUri. @@ -362,6 +364,7 @@ public function provideInputForToHash() 'alternativeText' => 'This is so Sindelfingen!', 'imageId' => '123-12345', 'uri' => 'http://' . $this->getImageInputPath(), + 'mime' => null, ] ), [ @@ -376,6 +379,7 @@ public function provideInputForToHash() 'width' => null, 'height' => null, 'additionalData' => [], + 'mime' => null, ], ], ];