diff --git a/src/lib/Persistence/Legacy/Content/FieldHandler.php b/src/lib/Persistence/Legacy/Content/FieldHandler.php index 393566d7cd..7f46d36fb7 100644 --- a/src/lib/Persistence/Legacy/Content/FieldHandler.php +++ b/src/lib/Persistence/Legacy/Content/FieldHandler.php @@ -14,6 +14,7 @@ use Ibexa\Contracts\Core\Persistence\Content\Type\FieldDefinition; use Ibexa\Contracts\Core\Persistence\Content\UpdateStruct; use Ibexa\Contracts\Core\Persistence\Content\VersionInfo; +use Ibexa\Core\FieldType\FieldTypeAliasResolverInterface; use Ibexa\Core\Persistence\FieldTypeRegistry; /** @@ -59,6 +60,8 @@ class FieldHandler */ protected $fieldTypes; + private FieldTypeAliasResolverInterface $fieldTypeAliasResolver; + /** * Creates a new Field Handler. * @@ -67,19 +70,22 @@ class FieldHandler * @param \Ibexa\Core\Persistence\Legacy\Content\StorageHandler $storageHandler * @param \Ibexa\Contracts\Core\Persistence\Content\Language\Handler $languageHandler * @param \Ibexa\Core\Persistence\FieldTypeRegistry $fieldTypeRegistry + * @param \Ibexa\Core\FieldType\FieldTypeAliasResolverInterface $fieldTypeAliasResolver */ public function __construct( Gateway $contentGateway, Mapper $mapper, StorageHandler $storageHandler, LanguageHandler $languageHandler, - FieldTypeRegistry $fieldTypeRegistry + FieldTypeRegistry $fieldTypeRegistry, + FieldTypeAliasResolverInterface $fieldTypeAliasResolver ) { $this->contentGateway = $contentGateway; $this->mapper = $mapper; $this->storageHandler = $storageHandler; $this->languageHandler = $languageHandler; $this->fieldTypeRegistry = $fieldTypeRegistry; + $this->fieldTypeAliasResolver = $fieldTypeAliasResolver; } /** @@ -454,7 +460,11 @@ protected function getFieldMap(array $fields, &$languageCodes = null) */ public function deleteFields($contentId, VersionInfo $versionInfo) { - foreach ($this->contentGateway->getFieldIdsByType($contentId, $versionInfo->versionNo) as $fieldType => $ids) { + $fieldTypeIdsMap = $this->resolveFieldTypeIdentifiers( + $this->contentGateway->getFieldIdsByType($contentId, $versionInfo->versionNo) + ); + + foreach ($fieldTypeIdsMap as $fieldType => $ids) { $this->storageHandler->deleteFieldData($fieldType, $versionInfo, $ids); } $this->contentGateway->deleteFields($contentId, $versionInfo->versionNo); @@ -471,10 +481,12 @@ public function deleteTranslationFromContentFields($contentId, array $versions, { foreach ($versions as $versionInfo) { // FT-specific implementations require VersionInfo to delete data - $fieldTypeIdsMap = $this->contentGateway->getFieldIdsByType( - $versionInfo->contentInfo->id, - $versionInfo->versionNo, - $languageCode + $fieldTypeIdsMap = $this->resolveFieldTypeIdentifiers( + $this->contentGateway->getFieldIdsByType( + $versionInfo->contentInfo->id, + $versionInfo->versionNo, + $languageCode + ) ); foreach ($fieldTypeIdsMap as $fieldType => $ids) { @@ -493,10 +505,12 @@ public function deleteTranslationFromContentFields($contentId, array $versions, */ public function deleteTranslationFromVersionFields(VersionInfo $versionInfo, $languageCode) { - $fieldTypeIdsMap = $this->contentGateway->getFieldIdsByType( - $versionInfo->contentInfo->id, - $versionInfo->versionNo, - $languageCode + $fieldTypeIdsMap = $this->resolveFieldTypeIdentifiers( + $this->contentGateway->getFieldIdsByType( + $versionInfo->contentInfo->id, + $versionInfo->versionNo, + $languageCode + ) ); foreach ($fieldTypeIdsMap as $fieldType => $ids) { $this->storageHandler->deleteFieldData($fieldType, $versionInfo, $ids); @@ -507,4 +521,26 @@ public function deleteTranslationFromVersionFields(VersionInfo $versionInfo, $la $versionInfo->versionNo ); } + + /** + * @param array $fieldTypeIdsMap + * + * @return array + */ + private function resolveFieldTypeIdentifiers(array $fieldTypeIdsMap): array + { + $resolvedFieldTypeIdsMap = []; + foreach ($fieldTypeIdsMap as $fieldTypeIdentifier => $ids) { + $resolvedFieldTypeIdentifier = $this->fieldTypeAliasResolver->resolveIdentifier( + $fieldTypeIdentifier + ); + + $resolvedFieldTypeIdsMap[$resolvedFieldTypeIdentifier] = array_merge( + $resolvedFieldTypeIdsMap[$resolvedFieldTypeIdentifier] ?? [], + $ids + ); + } + + return $resolvedFieldTypeIdsMap; + } } diff --git a/src/lib/Resources/settings/storage_engines/legacy/content.yml b/src/lib/Resources/settings/storage_engines/legacy/content.yml index cff5ea998f..cbc6f63d17 100644 --- a/src/lib/Resources/settings/storage_engines/legacy/content.yml +++ b/src/lib/Resources/settings/storage_engines/legacy/content.yml @@ -47,6 +47,7 @@ services: - '@Ibexa\Core\Persistence\Legacy\Content\StorageHandler' - '@ibexa.spi.persistence.legacy.language.handler' - '@Ibexa\Core\Persistence\FieldTypeRegistry' + - '@Ibexa\Core\FieldType\FieldTypeAliasResolverInterface' lazy: true Ibexa\Core\Persistence\Legacy\Content\TreeHandler: diff --git a/tests/integration/Core/Repository/FieldType/BaseIntegrationTestCase.php b/tests/integration/Core/Repository/FieldType/BaseIntegrationTestCase.php index 0dc30df3ab..3ccf06f60f 100644 --- a/tests/integration/Core/Repository/FieldType/BaseIntegrationTestCase.php +++ b/tests/integration/Core/Repository/FieldType/BaseIntegrationTestCase.php @@ -7,12 +7,14 @@ namespace Ibexa\Tests\Integration\Core\Repository\FieldType; +use Doctrine\DBAL\ParameterType; use Ibexa\Contracts\Core\Repository; use Ibexa\Contracts\Core\Repository\Exceptions\ContentTypeFieldDefinitionValidationException; use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException; use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Field; use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition; +use Ibexa\Core\Persistence\Legacy\Content\Gateway; use Ibexa\Tests\Integration\Core\Repository\BaseTestCase; /** @@ -953,6 +955,29 @@ public function testUpdateContentFails($failingValue, $expectedException) $this->updateContent($failingValue); } + protected function downgradeFieldTypeIdentifierToLegacyAlias( + string $legacyAlias, + int $contentId, + int $versionNo, + int $fieldDefinitionId + ): void { + $connection = $this->getRawDatabaseConnection(); + + $query = $connection->createQueryBuilder(); + $query + ->update(Gateway::CONTENT_FIELD_TABLE) + ->set('data_type_string', ':data_type_string') + ->setParameter('data_type_string', $legacyAlias, ParameterType::STRING) + ->andWhere('content_type_field_definition_id = :content_type_field_definition_id') + ->andWhere('version = :version') + ->andWhere('contentobject_id = :contentobject_id') + ->setParameter('content_type_field_definition_id', $fieldDefinitionId, ParameterType::INTEGER) + ->setParameter('version', $versionNo, ParameterType::INTEGER) + ->setParameter('contentobject_id', $contentId, ParameterType::INTEGER); + + $query->executeStatement(); + } + protected function removeFieldDefinition() { $repository = $this->getRepository(); diff --git a/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php b/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php index 98b190b7e1..a3cc4de20b 100644 --- a/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php +++ b/tests/integration/Core/Repository/FieldType/ImageIntegrationTest.php @@ -842,6 +842,44 @@ public function testDeleteImageWithCorruptedName(): void // Expect no League\Flysystem\CorruptedPathDetected thrown } + public function testDeleteContentRemovesImageStoredUnderLegacyFieldTypeIdentifier(): void + { + $ioService = $this->getSetupFactory()->getServiceContainer()->get(LegacyIOService::class); + $repository = $this->getRepository(); + $contentService = $repository->getContentService(); + + self::assertInstanceOf(IOServiceInterface::class, $ioService); + + $content = $this->publishNewImage( + __METHOD__, + new ImageValue( + [ + 'inputUri' => __DIR__ . '/_fixtures/image.jpg', + 'fileName' => 'image.jpg', + 'fileSize' => filesize(__DIR__ . '/_fixtures/image.jpg'), + 'alternativeText' => 'Alternative', + ] + ), + [2] + ); + + $imageFieldDefinition = $content->getContentType()->getFieldDefinition('image'); + self::assertNotNull($imageFieldDefinition); + + $this->assertImageExists(true, $ioService, $content); + + $this->downgradeFieldTypeIdentifierToLegacyAlias( + 'ezimage', + $content->id, + $content->getVersionInfo()->versionNo, + $imageFieldDefinition->id + ); + + $contentService->deleteContent($content->getVersionInfo()->getContentInfo()); + + $this->assertImageExists(false, $ioService, $content); + } + /** * @return array * diff --git a/tests/integration/Core/Repository/FieldType/UserIntegrationTest.php b/tests/integration/Core/Repository/FieldType/UserIntegrationTest.php index 42149e7089..0616640aa3 100644 --- a/tests/integration/Core/Repository/FieldType/UserIntegrationTest.php +++ b/tests/integration/Core/Repository/FieldType/UserIntegrationTest.php @@ -8,6 +8,7 @@ namespace Ibexa\Tests\Integration\Core\Repository\FieldType; use Doctrine\DBAL\Exception\NotNullConstraintViolationException; +use Doctrine\DBAL\ParameterType; use Ibexa\Contracts\Core\Repository\Exceptions\BadStateException; use Ibexa\Contracts\Core\Repository\Exceptions\ForbiddenException; use Ibexa\Contracts\Core\Repository\Values\Content\Field; @@ -15,6 +16,7 @@ use Ibexa\Contracts\Core\Repository\Values\ContentType\FieldDefinition; use Ibexa\Core\FieldType\User\Type; use Ibexa\Core\FieldType\User\Value as UserValue; +use Ibexa\Core\Persistence\Legacy\User\Gateway as UserGateway; use Ibexa\Core\Repository\Values\User\User; use Ibexa\Tests\Core\FieldType\DataProvider\UserValidatorConfigurationSchemaProvider; @@ -511,6 +513,47 @@ public function testUpdateFieldDefinitionWithIncompleteSettingsSchema() self::assertNull($userFieldDefinition->fieldSettings[Type::PASSWORD_TTL_WARNING_SETTING]); } + public function testDeleteContentRemovesUserAccountStoredUnderLegacyFieldTypeIdentifier(): void + { + $repository = $this->getRepository(); + $contentService = $repository->getContentService(); + $contentTypeService = $repository->getContentTypeService(); + + $user = $this->createUserVersion1('legacy-identifier-user'); + $contentInfo = $user->getContentInfo(); + + self::assertTrue($this->userAccountExists($contentInfo->getId())); + + $userFieldDefinition = $this->getUserFieldDefinition( + $contentTypeService->loadContentType($contentInfo->contentTypeId) + ); + + $this->downgradeFieldTypeIdentifierToLegacyAlias( + 'ezuser', + $contentInfo->getId(), + $contentInfo->currentVersionNo, + $userFieldDefinition->id + ); + + $contentService->deleteContent($contentInfo); + + self::assertFalse($this->userAccountExists($contentInfo->getId())); + } + + private function userAccountExists(int $contentId): bool + { + $connection = $this->getRawDatabaseConnection(); + + $query = $connection->createQueryBuilder(); + $query + ->select('COUNT(contentobject_id)') + ->from(UserGateway::USER_TABLE) + ->where('contentobject_id = :contentobject_id') + ->setParameter('contentobject_id', $contentId, ParameterType::INTEGER); + + return (int)$query->executeQuery()->fetchOne() > 0; + } + /** * Finds ibexa_user field definition in given $contentType or mark test as failed if it doens't exists. * diff --git a/tests/lib/Persistence/Legacy/Content/FieldHandlerTest.php b/tests/lib/Persistence/Legacy/Content/FieldHandlerTest.php index cd65e130c6..5e53fd5dde 100644 --- a/tests/lib/Persistence/Legacy/Content/FieldHandlerTest.php +++ b/tests/lib/Persistence/Legacy/Content/FieldHandlerTest.php @@ -16,6 +16,9 @@ use Ibexa\Contracts\Core\Persistence\Content\UpdateStruct; use Ibexa\Contracts\Core\Persistence\Content\VersionInfo; use Ibexa\Contracts\Core\Persistence\FieldType as SPIFieldType; +use Ibexa\Core\FieldType\FieldTypeAliasRegistry; +use Ibexa\Core\FieldType\FieldTypeAliasResolver; +use Ibexa\Core\FieldType\FieldTypeAliasResolverInterface; use Ibexa\Core\Persistence\FieldTypeRegistry; use Ibexa\Core\Persistence\Legacy\Content\FieldHandler; use Ibexa\Core\Persistence\Legacy\Content\Gateway; @@ -28,6 +31,16 @@ */ class FieldHandlerTest extends LanguageAwareTestCase { + private const EXTERNAL_STORAGE_FIELD_TYPE_ALIASES = [ + 'ezbinaryfile' => 'ibexa_binaryfile', + 'ezgmaplocation' => 'ibexa_gmap_location', + 'ezimage' => 'ibexa_image', + 'ezkeyword' => 'ibexa_keyword', + 'ezmedia' => 'ibexa_media', + 'ezurl' => 'ibexa_url', + 'ezuser' => 'ibexa_user', + ]; + /** * Gateway mock. * @@ -824,6 +837,115 @@ public function testDeleteFields() $fieldHandler->deleteFields(42, new VersionInfo(['versionNo' => 2])); } + /** + * @dataProvider provideLegacyFieldTypeIdentifiers + */ + public function testDeleteFieldsResolvesLegacyFieldTypeIdentifier( + string $legacyIdentifier, + string $expectedIdentifier + ): void { + $fieldHandler = $this->getFieldHandler(); + + $contentGatewayMock = $this->getContentGatewayMock(); + $contentGatewayMock->expects(self::once()) + ->method('getFieldIdsByType') + ->with(self::equalTo(42), self::equalTo(2)) + ->willReturn([$legacyIdentifier => [2, 3]]); + + $storageHandlerMock = $this->getStorageHandlerMock(); + $storageHandlerMock->expects(self::once()) + ->method('deleteFieldData') + ->with( + self::equalTo($expectedIdentifier), + self::isInstanceOf(VersionInfo::class), + self::equalTo([2, 3]) + ); + + $fieldHandler->deleteFields(42, new VersionInfo(['versionNo' => 2])); + } + + /** + * @return iterable + */ + public function provideLegacyFieldTypeIdentifiers(): iterable + { + foreach (self::EXTERNAL_STORAGE_FIELD_TYPE_ALIASES as $legacyAlias => $alias) { + yield $legacyAlias => [$legacyAlias, $alias]; + } + + yield 'already migrated identifier' => ['ibexa_image', 'ibexa_image']; + } + + public function testDeleteFieldsMergesLegacyAndCurrentFieldTypeIdentifiers(): void + { + $fieldHandler = $this->getFieldHandler(); + + $contentGatewayMock = $this->getContentGatewayMock(); + $contentGatewayMock->expects(self::once()) + ->method('getFieldIdsByType') + ->with(self::equalTo(42), self::equalTo(2)) + ->willReturn(['ezimage' => [2], 'ibexa_image' => [3]]); + + $storageHandlerMock = $this->getStorageHandlerMock(); + $storageHandlerMock->expects(self::once()) + ->method('deleteFieldData') + ->with( + self::equalTo('ibexa_image'), + self::isInstanceOf(VersionInfo::class), + self::equalTo([2, 3]) + ); + + $fieldHandler->deleteFields(42, new VersionInfo(['versionNo' => 2])); + } + + public function testDeleteTranslationFromVersionFieldsResolvesLegacyFieldTypeIdentifier(): void + { + $fieldHandler = $this->getFieldHandler(); + $versionInfo = new VersionInfo([ + 'contentInfo' => new ContentInfo(['id' => 42]), + 'versionNo' => 2, + ]); + + $this->getContentGatewayMock()->expects(self::once()) + ->method('getFieldIdsByType') + ->with(self::equalTo(42), self::equalTo(2), self::equalTo('eng-GB')) + ->willReturn(['ezimage' => [4]]); + + $this->getStorageHandlerMock()->expects(self::once()) + ->method('deleteFieldData') + ->with( + self::equalTo('ibexa_image'), + self::isInstanceOf(VersionInfo::class), + self::equalTo([4]) + ); + + $fieldHandler->deleteTranslationFromVersionFields($versionInfo, 'eng-GB'); + } + + public function testDeleteTranslationFromContentFieldsResolvesLegacyFieldTypeIdentifier(): void + { + $fieldHandler = $this->getFieldHandler(); + $versionInfo = new VersionInfo([ + 'contentInfo' => new ContentInfo(['id' => 42]), + 'versionNo' => 2, + ]); + + $this->getContentGatewayMock()->expects(self::once()) + ->method('getFieldIdsByType') + ->with(self::equalTo(42), self::equalTo(2), self::equalTo('eng-GB')) + ->willReturn(['ezuser' => [5]]); + + $this->getStorageHandlerMock()->expects(self::once()) + ->method('deleteFieldData') + ->with( + self::equalTo('ibexa_user'), + self::isInstanceOf(VersionInfo::class), + self::equalTo([5]) + ); + + $fieldHandler->deleteTranslationFromContentFields(42, [$versionInfo], 'eng-GB'); + } + /** * Returns a Content fixture. * @@ -1034,12 +1156,23 @@ protected function getFieldHandler() $this->getMapperMock(), $this->getStorageHandlerMock(), $this->getLanguageHandler(), - $this->getFieldTypeRegistryMock() + $this->getFieldTypeRegistryMock(), + $this->getFieldTypeAliasResolver() ); return $mock; } + protected function getFieldTypeAliasResolver(): FieldTypeAliasResolverInterface + { + $fieldTypeAliasRegistry = new FieldTypeAliasRegistry(); + foreach (self::EXTERNAL_STORAGE_FIELD_TYPE_ALIASES as $legacyAlias => $alias) { + $fieldTypeAliasRegistry->register($legacyAlias, $alias); + } + + return new FieldTypeAliasResolver($fieldTypeAliasRegistry); + } + /** * Returns a StorageHandler mock. *