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;
}