diff --git a/Repository/ContentService.php b/Repository/ContentService.php index 30c14e4e0..794c136fd 100644 --- a/Repository/ContentService.php +++ b/Repository/ContentService.php @@ -150,6 +150,23 @@ public function loadContent($contentId, array $languages = null, $versionNo = nu */ public function loadContentByRemoteId($remoteId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true); + /** + * Bulk-load Content items by the list of ContentInfo Value Objects. + * + * Note: it does not throw exceptions on load, just ignores erroneous Content item. + * Moreover, since the method works on pre-loaded ContentInfo list, it is assumed that user is + * allowed to access every Content on the list. + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo[] $contentInfoList + * @param string[] $languages A language priority, filters returned fields and is used as prioritized language code on + * returned value object. If not given all languages are returned. + * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true, + * unless all languages have been asked for. + * + * @return \eZ\Publish\API\Repository\Values\Content\Content[] list of Content items with Content Ids as keys + */ + public function loadContentListByContentInfo(array $contentInfoList, array $languages = [], $useAlwaysAvailable = true); + /** * Creates a new content draft assigned to the authenticated user. * diff --git a/Repository/LocationService.php b/Repository/LocationService.php index e9035782b..762537eeb 100644 --- a/Repository/LocationService.php +++ b/Repository/LocationService.php @@ -212,4 +212,23 @@ public function newLocationCreateStruct($parentLocationId); * @return \eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct */ public function newLocationUpdateStruct(); + + /** + * Get the total number of all existing Locations. Can be combined with loadAllLocations. + * + * @see loadAllLocations + * + * @return int Total number of Locations + */ + public function getAllLocationsCount(): int; + + /** + * Bulk-load all existing Locations, constrained by $limit and $offset to paginate results. + * + * @param int $limit + * @param int $offset + * + * @return \eZ\Publish\API\Repository\Values\Content\Location[] + */ + public function loadAllLocations(int $offset = 0, int $limit = 25): array; } diff --git a/Repository/Tests/BaseTest.php b/Repository/Tests/BaseTest.php index c2a757ee7..67f674946 100644 --- a/Repository/Tests/BaseTest.php +++ b/Repository/Tests/BaseTest.php @@ -10,6 +10,7 @@ use eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException; use eZ\Publish\API\Repository\Tests\PHPUnitConstraint\ValidationErrorOccurs as PHPUnitConstraintValidationErrorOccurs; +use eZ\Publish\API\Repository\Values\Content\Location; use EzSystems\EzPlatformSolrSearchEngine\Tests\SetupFactory\LegacySetupFactory as LegacySolrSetupFactory; use PHPUnit\Framework\TestCase; use eZ\Publish\API\Repository\Repository; @@ -622,4 +623,43 @@ protected function assertValidationErrorOccurs( self::assertThat($exception, $constraint); } + + /** + * Create 'folder' Content. + * + * @param array $names Folder names in the form of ['<language_code>' => '<name>'] + * @param int $parentLocationId + * + * @return \eZ\Publish\API\Repository\Values\Content\Content published Content + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + protected function createFolder(array $names, $parentLocationId) + { + $repository = $this->getRepository(false); + $contentService = $repository->getContentService(); + $contentTypeService = $repository->getContentTypeService(); + $locationService = $repository->getLocationService(); + + if (empty($names)) { + throw new \RuntimeException(sprintf('%s expects non-empty names list', __METHOD__)); + } + $mainLanguageCode = array_keys($names)[0]; + + $struct = $contentService->newContentCreateStruct( + $contentTypeService->loadContentTypeByIdentifier('folder'), + $mainLanguageCode + ); + foreach ($names as $languageCode => $translatedName) { + $struct->setField('name', $translatedName, $languageCode); + } + $contentDraft = $contentService->createContent( + $struct, + [$locationService->newLocationCreateStruct($parentLocationId)] + ); + + return $contentService->publishVersion($contentDraft->versionInfo); + } } diff --git a/Repository/Tests/Common/SlugConverter.php b/Repository/Tests/Common/SlugConverter.php new file mode 100644 index 000000000..6e29bcd3e --- /dev/null +++ b/Repository/Tests/Common/SlugConverter.php @@ -0,0 +1,28 @@ +configuration[$key] = $value; + } +} diff --git a/Repository/Tests/ContentServiceTest.php b/Repository/Tests/ContentServiceTest.php index ead2d9415..7815d9aec 100644 --- a/Repository/Tests/ContentServiceTest.php +++ b/Repository/Tests/ContentServiceTest.php @@ -9,6 +9,7 @@ namespace eZ\Publish\API\Repository\Tests; use eZ\Publish\API\Repository\Values\Content\Content; +use eZ\Publish\API\Repository\Exceptions\UnauthorizedException; use eZ\Publish\API\Repository\Values\Content\ContentInfo; use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct; use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct; @@ -27,7 +28,7 @@ /** * Test case for operations in the ContentService using in memory storage. * - * @see eZ\Publish\API\Repository\ContentService + * @see \eZ\Publish\API\Repository\ContentService * @group content */ class ContentServiceTest extends BaseContentServiceTest @@ -5737,6 +5738,37 @@ public function testDeleteTranslationFromDraftThrowsInvalidArgumentException() $contentService->deleteTranslationFromDraft($draft->versionInfo, $languageCode); } + /** + * Test loading list of Content items. + */ + public function testLoadContentListByContentInfo() + { + $repository = $this->getRepository(); + $contentService = $repository->getContentService(); + $locationService = $repository->getLocationService(); + + $allLocationsCount = $locationService->getAllLocationsCount(); + $contentInfoList = array_map( + function (Location $location) { + return $location->contentInfo; + }, + $locationService->loadAllLocations(0, $allLocationsCount) + ); + + $contentList = $contentService->loadContentListByContentInfo($contentInfoList); + self::assertCount(count($contentInfoList), $contentList); + foreach ($contentList as $content) { + try { + $loadedContent = $contentService->loadContent($content->id); + self::assertEquals($loadedContent, $content, "Failed to properly bulk-load Content {$content->id}"); + } catch (NotFoundException $e) { + self::fail("Failed to load Content {$content->id}: {$e->getMessage()}"); + } catch (UnauthorizedException $e) { + self::fail("Failed to load Content {$content->id}: {$e->getMessage()}"); + } + } + } + /** * Asserts that all aliases defined in $expectedAliasProperties with the * given properties are available in $actualAliases and not more. diff --git a/Repository/Tests/SetupFactory/Legacy.php b/Repository/Tests/SetupFactory/Legacy.php index 077596525..76eea5357 100644 --- a/Repository/Tests/SetupFactory/Legacy.php +++ b/Repository/Tests/SetupFactory/Legacy.php @@ -391,6 +391,9 @@ public function getServiceContainer() $containerBuilder->addCompilerPass(new Compiler\Search\SearchEngineSignalSlotPass('legacy')); $containerBuilder->addCompilerPass(new Compiler\Search\FieldRegistryPass()); + // load overrides just before creating test Container + $loader->load('tests/override.yml'); + self::$serviceContainer = new ServiceContainer( $containerBuilder, $installDir, diff --git a/Repository/Tests/SetupFactory/LegacyElasticsearch.php b/Repository/Tests/SetupFactory/LegacyElasticsearch.php index 4f135d90f..b4d9509f2 100644 --- a/Repository/Tests/SetupFactory/LegacyElasticsearch.php +++ b/Repository/Tests/SetupFactory/LegacyElasticsearch.php @@ -69,6 +69,9 @@ public function getServiceContainer() self::$ioRootDir . '/' . $containerBuilder->getParameter('storage_dir') ); + // load overrides just before creating test Container + $loader->load('tests/override.yml'); + self::$serviceContainer = new ServiceContainer( $containerBuilder, $installDir, diff --git a/Repository/Tests/URLAliasServiceTest.php b/Repository/Tests/URLAliasServiceTest.php index 3cc186db4..1daec52f6 100644 --- a/Repository/Tests/URLAliasServiceTest.php +++ b/Repository/Tests/URLAliasServiceTest.php @@ -8,8 +8,16 @@ */ namespace eZ\Publish\API\Repository\Tests; +use Doctrine\DBAL\Connection; +use eZ\Publish\API\Repository\Exceptions\InvalidArgumentException; +use eZ\Publish\API\Repository\Exceptions\NotFoundException; +use eZ\Publish\API\Repository\Tests\Common\SlugConverter as TestSlugConverter; +use eZ\Publish\API\Repository\Values\Content\ContentInfo; +use eZ\Publish\API\Repository\Values\Content\Location; use eZ\Publish\API\Repository\Values\Content\URLAlias; use Exception; +use PDO; +use RuntimeException; /** * Test case for operations in the URLAliasService using in memory storage. @@ -1029,4 +1037,464 @@ public function testLookupOnRenamedParent() $this->assertEquals('/My-Folder-Modified/My-Article', $aliases[0]->path); } + + /** + * Test lookup on multilingual nested Locations returns proper UrlAlias Value. + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + public function testLookupOnMultilingualNestedLocations() + { + $urlAliasService = $this->getRepository()->getURLAliasService(); + $locationService = $this->getRepository()->getLocationService(); + + $topFolderNames = [ + 'eng-GB' => 'My folder Name', + 'ger-DE' => 'Ger folder Name', + 'eng-US' => 'My folder Name', + ]; + $nestedFolderNames = [ + 'eng-GB' => 'nested Folder name', + 'ger-DE' => 'Ger Nested folder Name', + 'eng-US' => 'nested Folder name', + ]; + $topFolderLocation = $locationService->loadLocation( + $this->createFolder($topFolderNames, 2)->contentInfo->mainLocationId + ); + $nestedFolderLocation = $locationService->loadLocation( + $this->createFolder( + $nestedFolderNames, + $topFolderLocation->id + )->contentInfo->mainLocationId + ); + $urlAlias = $urlAliasService->lookup('/My-Folder-Name/Nested-Folder-Name'); + self::assertPropertiesCorrect( + [ + 'destination' => $nestedFolderLocation->id, + 'path' => '/My-folder-Name/nested-Folder-name', + 'languageCodes' => ['eng-US', 'eng-GB'], + 'isHistory' => false, + 'isCustom' => false, + 'forward' => false, + ], + $urlAlias + ); + $urlAlias = $urlAliasService->lookup('/Ger-Folder-Name/Ger-Nested-Folder-Name'); + self::assertPropertiesCorrect( + [ + 'destination' => $nestedFolderLocation->id, + 'path' => '/Ger-folder-Name/Ger-Nested-folder-Name', + 'languageCodes' => ['ger-DE'], + 'isHistory' => false, + 'isCustom' => false, + 'forward' => false, + ], + $urlAlias + ); + + return [$topFolderLocation, $nestedFolderLocation]; + } + + /** + * Test refreshSystemUrlAliasesForLocation historizes and changes current URL alias after + * changing SlugConverter configuration. + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + * @throws \ErrorException + */ + public function testRefreshSystemUrlAliasesForLocationWithChangedSlugConverterConfiguration() + { + list($topFolderLocation, $nestedFolderLocation) = $this->testLookupOnMultilingualNestedLocations(); + + $urlAliasService = $this->getRepository(false)->getURLAliasService(); + + $this->changeSlugConverterConfiguration('transformation', 'urlalias_compat'); + $this->changeSlugConverterConfiguration('wordSeparatorName', 'underscore'); + + try { + $urlAliasService->refreshSystemUrlAliasesForLocation($topFolderLocation); + $urlAliasService->refreshSystemUrlAliasesForLocation($nestedFolderLocation); + + $urlAlias = $urlAliasService->lookup('/My-Folder-Name/Nested-Folder-Name'); + $this->assertUrlAliasPropertiesCorrect( + $nestedFolderLocation, + '/My-folder-Name/nested-Folder-name', + ['eng-US', 'eng-GB'], + true, + $urlAlias + ); + + $urlAlias = $urlAliasService->lookup('/my_folder_name/nested_folder_name'); + $this->assertUrlAliasPropertiesCorrect( + $nestedFolderLocation, + '/my_folder_name/nested_folder_name', + ['eng-US', 'eng-GB'], + false, + $urlAlias + ); + + $urlAlias = $urlAliasService->lookup('/Ger-Folder-Name/Ger-Nested-Folder-Name'); + $this->assertUrlAliasPropertiesCorrect( + $nestedFolderLocation, + '/Ger-folder-Name/Ger-Nested-folder-Name', + ['ger-DE'], + true, + $urlAlias + ); + + $urlAlias = $urlAliasService->lookup('/ger_folder_name/ger_nested_folder_name'); + $this->assertUrlAliasPropertiesCorrect( + $nestedFolderLocation, + '/ger_folder_name/ger_nested_folder_name', + ['ger-DE'], + false, + $urlAlias + ); + } finally { + // restore configuration + $this->changeSlugConverterConfiguration('transformation', 'urlalias'); + $this->changeSlugConverterConfiguration('wordSeparatorName', 'dash'); + } + } + + /** + * Test that URL aliases are refreshed after changing URL alias schema Field name of a Content Type. + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + public function testRefreshSystemUrlAliasesForContentsWithUpdatedContentTypes() + { + list($topFolderLocation, $nestedFolderLocation) = $this->testLookupOnMultilingualNestedLocations(); + /** @var \eZ\Publish\API\Repository\Values\Content\Location $topFolderLocation */ + /** @var \eZ\Publish\API\Repository\Values\Content\Location $nestedFolderLocation */ + + // Default URL Alias schema is which messes up this test, so: + $this->changeContentTypeUrlAliasSchema('folder', ''); + + $urlAliasService = $this->getRepository(false)->getURLAliasService(); + + $this->updateContentField( + $topFolderLocation->getContentInfo(), + 'short_name', + ['eng-GB' => 'EN Short Name', 'ger-DE' => 'DE Short Name'] + ); + $this->updateContentField( + $nestedFolderLocation->getContentInfo(), + 'short_name', + ['eng-GB' => 'EN Nested Short Name', 'ger-DE' => 'DE Nested Short Name'] + ); + + $this->changeContentTypeUrlAliasSchema('folder', ''); + + // sanity test, done after updating CT, because it does not update existing entries by design + $this->assertUrlIsCurrent('/My-folder-Name', $topFolderLocation->id); + $this->assertUrlIsCurrent('/Ger-folder-Name', $topFolderLocation->id); + $this->assertUrlIsCurrent('/My-folder-Name/nested-Folder-name', $nestedFolderLocation->id); + $this->assertUrlIsCurrent('/Ger-folder-Name/Ger-Nested-folder-Name', $nestedFolderLocation->id); + + // Call API being tested + $urlAliasService->refreshSystemUrlAliasesForLocation($topFolderLocation); + $urlAliasService->refreshSystemUrlAliasesForLocation($nestedFolderLocation); + + // check archived aliases + $this->assertUrlIsHistory('/My-folder-Name', $topFolderLocation->id); + $this->assertUrlIsHistory('/Ger-folder-Name', $topFolderLocation->id); + $this->assertUrlIsHistory('/My-folder-Name/nested-Folder-name', $nestedFolderLocation->id); + $this->assertUrlIsHistory('/Ger-folder-Name/Ger-Nested-folder-Name', $nestedFolderLocation->id); + + // check new current aliases + $this->assertUrlIsCurrent('/EN-Short-Name', $topFolderLocation->id); + $this->assertUrlIsCurrent('/DE-Short-Name', $topFolderLocation->id); + $this->assertUrlIsCurrent('/EN-Short-Name/EN-Nested-Short-Name', $nestedFolderLocation->id); + $this->assertUrlIsCurrent('/DE-Short-Name/DE-Nested-Short-Name', $nestedFolderLocation->id); + } + + /** + * Lookup given URL and check if it is archived and points to the given Location Id. + * + * @param string $lookupUrl + * @param int $expectedDestination Expected Location ID + */ + protected function assertUrlIsHistory($lookupUrl, $expectedDestination) + { + $this->assertLookupHistory(true, $expectedDestination, $lookupUrl); + } + + /** + * Lookup given URL and check if it is current (not archived) and points to the given Location Id. + * + * @param string $lookupUrl + * @param int $expectedDestination Expected Location ID + */ + protected function assertUrlIsCurrent($lookupUrl, $expectedDestination) + { + $this->assertLookupHistory(false, $expectedDestination, $lookupUrl); + } + + /** + * Lookup and URLAlias VO history and destination properties. + * + * @see assertUrlIsHistory + * @see assertUrlIsCurrent + * + * @param bool $expectedIsHistory + * @param int $expectedDestination Expected Location ID + * @param string $lookupUrl + */ + protected function assertLookupHistory($expectedIsHistory, $expectedDestination, $lookupUrl) + { + $urlAliasService = $this->getRepository(false)->getURLAliasService(); + + try { + $urlAlias = $urlAliasService->lookup($lookupUrl); + self::assertPropertiesCorrect( + [ + 'destination' => $expectedDestination, + 'path' => $lookupUrl, + 'isHistory' => $expectedIsHistory, + ], + $urlAlias + ); + } catch (InvalidArgumentException $e) { + self::fail("Failed to lookup {$lookupUrl}: $e"); + } catch (NotFoundException $e) { + self::fail("Failed to lookup {$lookupUrl}: $e"); + } + } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + * @param $fieldDefinitionIdentifier + * @param array $fieldValues + * + * @return \eZ\Publish\API\Repository\Values\Content\Content + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + protected function updateContentField(ContentInfo $contentInfo, $fieldDefinitionIdentifier, array $fieldValues) + { + $contentService = $this->getRepository(false)->getContentService(); + + $contentUpdateStruct = $contentService->newContentUpdateStruct(); + foreach ($fieldValues as $languageCode => $fieldValue) { + $contentUpdateStruct->setField($fieldDefinitionIdentifier, $fieldValue, $languageCode); + } + $contentDraft = $contentService->updateContent( + $contentService->createContentDraft($contentInfo)->versionInfo, + $contentUpdateStruct + ); + + return $contentService->publishVersion($contentDraft->versionInfo); + } + + /** + * Test deleting corrupted URL aliases. + * + * Note: this test will not be needed once we introduce Improved Storage with Foreign keys support. + * + * Note: test depends on already broken URL aliases: eznode:59, eznode:59, eznode:60. + * + * @throws \ErrorException + */ + public function testDeleteCorruptedUrlAliases() + { + $repository = $this->getRepository(); + $urlAliasService = $repository->getURLAliasService(); + $connection = $this->getRawDatabaseConnection(); + + $query = $connection->createQueryBuilder()->select('*')->from('ezurlalias_ml'); + $originalRows = $query->execute()->fetchAll(PDO::FETCH_ASSOC); + + $expectedCount = count($originalRows); + $expectedCount += $this->insertBrokenUrlAliasTableFixtures($connection); + + // sanity check + $updatedRows = $query->execute()->fetchAll(PDO::FETCH_ASSOC); + self::assertCount($expectedCount, $updatedRows, 'Found unexpected number of new rows'); + + // BEGIN API use case + $urlAliasService->deleteCorruptedUrlAliases(); + // END API use case + + $updatedRows = $query->execute()->fetchAll(PDO::FETCH_ASSOC); + self::assertCount( + // API should also remove already broken pre-existing URL aliases to Locations 50 and 2x 59 + count($originalRows) - 3, + $updatedRows, + 'Number of rows after cleanup is not the same as the original number of rows' + ); + } + + /** + * Mutate 'ezpublish.persistence.slug_converter' Service configuration. + * + * @param string $key + * @param string $value + * + * @throws \ErrorException + * @throws \Exception + */ + protected function changeSlugConverterConfiguration($key, $value) + { + $testSlugConverter = $this + ->getSetupFactory() + ->getServiceContainer() + ->getInnerContainer() + ->get('ezpublish.persistence.slug_converter'); + + if (!$testSlugConverter instanceof TestSlugConverter) { + throw new RuntimeException( + sprintf( + '%s: expected instance of %s, got %s', + __METHOD__, + TestSlugConverter::class, + get_class($testSlugConverter) + ) + ); + } + + $testSlugConverter->setConfigurationValue($key, $value); + } + + /** + * Update Content Type URL alias schema pattern. + * + * @param string $contentTypeIdentifier + * @param string $newUrlAliasSchema + * + * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException + * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException + * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException + */ + protected function changeContentTypeUrlAliasSchema($contentTypeIdentifier, $newUrlAliasSchema) + { + $contentTypeService = $this->getRepository(false)->getContentTypeService(); + + $contentType = $contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier); + + $contentTypeDraft = $contentTypeService->createContentTypeDraft($contentType); + $contentTypeUpdateStruct = $contentTypeService->newContentTypeUpdateStruct(); + $contentTypeUpdateStruct->urlAliasSchema = $newUrlAliasSchema; + + $contentTypeService->updateContentTypeDraft($contentTypeDraft, $contentTypeUpdateStruct); + $contentTypeService->publishContentTypeDraft($contentTypeDraft); + } + + private function assertUrlAliasPropertiesCorrect( + Location $expectedDestinationLocation, + $expectedPath, + array $expectedLanguageCodes, + $expectedIsHistory, + URLAlias $actualUrlAliasValue + ) { + self::assertPropertiesCorrect( + [ + 'destination' => $expectedDestinationLocation->id, + 'path' => $expectedPath, + // @todo uncomment after fixing EZP-27124 + //'languageCodes' => $expectedLanguageCodes, + 'isHistory' => $expectedIsHistory, + 'isCustom' => false, + 'forward' => false, + ], + $actualUrlAliasValue + ); + } + + /** + * @return \Doctrine\DBAL\Connection + * + * @throws \ErrorException + */ + private function getRawDatabaseConnection() + { + $connection = $this + ->getSetupFactory() + ->getServiceContainer()->get('ezpublish.api.storage_engine.legacy.connection'); + + if (!$connection instanceof Connection) { + throw new \RuntimeException( + sprintf('Expected %s got something else', Connection::class) + ); + } + + return $connection; + } + + /** + * Insert intentionally broken rows into ezurlalias_ml table to test cleanup API. + * + * @see \eZ\Publish\API\Repository\URLAliasService::deleteCorruptedUrlAliases + * @see testDeleteCorruptedUrlAliases + * + * @param \Doctrine\DBAL\Connection $connection + * + * @return int Number of new rows + */ + private function insertBrokenUrlAliasTableFixtures(Connection $connection) + { + $rows = [ + // link to non-existent location + [ + 'action' => 'eznode:9999', + 'action_type' => 'eznode', + 'alias_redirects' => 0, + 'id' => 9997, + 'is_alias' => 0, + 'is_original' => 1, + 'lang_mask' => 3, + 'link' => 9997, + 'parent' => 0, + 'text' => 'my-location', + 'text_md5' => '19d12b1b9994619cd8e90f00a6f5834e', + ], + // link to non-existent target URL alias (`link` column) + [ + 'action' => 'nop:', + 'action_type' => 'nop', + 'alias_redirects' => 0, + 'id' => 9998, + 'is_alias' => 1, + 'is_original' => 1, + 'lang_mask' => 2, + 'link' => 9995, + 'parent' => 0, + 'text' => 'my-alias1', + 'text_md5' => 'a29dd95ccf4c1bc7ebbd61086863b632', + ], + // link to non-existent parent URL alias + [ + 'action' => 'nop:', + 'action_type' => 'nop', + 'alias_redirects' => 0, + 'id' => 9999, + 'is_alias' => 0, + 'is_original' => 1, + 'lang_mask' => 3, + 'link' => 9999, + 'parent' => 9995, + 'text' => 'my-alias2', + 'text_md5' => 'e5dea18481e4f86857865d9fc94e4ce9', + ], + ]; + + $query = $connection->createQueryBuilder()->insert('ezurlalias_ml'); + + foreach ($rows as $row) { + foreach ($row as $columnName => $value) { + $row[$columnName] = $query->createNamedParameter($value); + } + $query->values($row); + $query->execute(); + } + + return count($rows); + } } diff --git a/Repository/URLAliasService.php b/Repository/URLAliasService.php index a1e7edf64..a843566b2 100644 --- a/Repository/URLAliasService.php +++ b/Repository/URLAliasService.php @@ -134,4 +134,18 @@ public function reverseLookup(Location $location, $languageCode = null); * @return \eZ\Publish\API\Repository\Values\Content\URLAlias */ public function load($id); + + /** + * Refresh all system URL aliases for the given Location (and historize outdated if needed). + * + * @param \eZ\Publish\API\Repository\Values\Content\Location $location + */ + public function refreshSystemUrlAliasesForLocation(Location $location): void; + + /** + * Delete global, system or custom URL alias pointing to non-existent Locations. + * + * @return int Number of deleted URL aliases + */ + public function deleteCorruptedUrlAliases(): int; }