vendor/ezsystems/ezplatform-kernel/eZ/Publish/Core/Repository/LocationService.php line 55

Open in your IDE?
  1. <?php
  2. /**
  3.  * @copyright Copyright (C) Ibexa AS. All rights reserved.
  4.  * @license For full copyright and license information view LICENSE file distributed with this source code.
  5.  */
  6. declare(strict_types=1);
  7. namespace eZ\Publish\Core\Repository;
  8. use eZ\Publish\API\Repository\ContentTypeService;
  9. use eZ\Publish\API\Repository\PermissionCriterionResolver;
  10. use eZ\Publish\API\Repository\PermissionResolver;
  11. use eZ\Publish\API\Repository\Values\Content\Language;
  12. use eZ\Publish\API\Repository\Values\Content\Location;
  13. use eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct;
  14. use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
  15. use eZ\Publish\API\Repository\Values\Content\ContentInfo;
  16. use eZ\Publish\API\Repository\Values\Content\Location as APILocation;
  17. use eZ\Publish\API\Repository\Values\Content\LocationList;
  18. use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LanguageCode;
  19. use eZ\Publish\API\Repository\Values\Content\VersionInfo;
  20. use eZ\Publish\Core\Repository\Mapper\ContentDomainMapper;
  21. use eZ\Publish\SPI\Limitation\Target;
  22. use eZ\Publish\SPI\Persistence\Content\Location as SPILocation;
  23. use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
  24. use eZ\Publish\API\Repository\LocationService as LocationServiceInterface;
  25. use eZ\Publish\API\Repository\Repository as RepositoryInterface;
  26. use eZ\Publish\SPI\Persistence\Filter\Location\Handler as LocationFilteringHandler;
  27. use eZ\Publish\SPI\Persistence\Handler;
  28. use eZ\Publish\API\Repository\Values\Content\Query;
  29. use eZ\Publish\API\Repository\Values\Content\LocationQuery;
  30. use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
  31. use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd as CriterionLogicalAnd;
  32. use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalNot as CriterionLogicalNot;
  33. use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Subtree as CriterionSubtree;
  34. use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
  35. use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
  36. use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
  37. use eZ\Publish\Core\Base\Exceptions\BadStateException;
  38. use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
  39. use Exception;
  40. use eZ\Publish\API\Repository\Values\Filter\Filter;
  41. use eZ\Publish\SPI\Repository\Values\Filter\FilteringCriterion;
  42. use Psr\Log\LoggerInterface;
  43. use Psr\Log\NullLogger;
  44. use eZ\Publish\API\Repository\Values\ContentType\ContentType;
  45. use function count;
  46. /**
  47.  * Location service, used for complex subtree operations.
  48.  *
  49.  * @example Examples/location.php
  50.  */
  51. class LocationService implements LocationServiceInterface
  52. {
  53.     /** @var \eZ\Publish\Core\Repository\Repository */
  54.     protected $repository;
  55.     /** @var \eZ\Publish\SPI\Persistence\Handler */
  56.     protected $persistenceHandler;
  57.     /** @var array */
  58.     protected $settings;
  59.     /** @var \eZ\Publish\Core\Repository\Mapper\ContentDomainMapper */
  60.     protected $contentDomainMapper;
  61.     /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
  62.     protected $nameSchemaService;
  63.     /** @var \eZ\Publish\API\Repository\PermissionCriterionResolver */
  64.     protected $permissionCriterionResolver;
  65.     /** @var \Psr\Log\LoggerInterface */
  66.     private $logger;
  67.     /** @var \eZ\Publish\API\Repository\PermissionResolver */
  68.     private $permissionResolver;
  69.     /** @var \eZ\Publish\SPI\Persistence\Filter\Location\Handler */
  70.     private $locationFilteringHandler;
  71.     /** @var \eZ\Publish\API\Repository\ContentTypeService */
  72.     protected $contentTypeService;
  73.     /**
  74.      * Setups service with reference to repository object that created it & corresponding handler.
  75.      *
  76.      * @param \eZ\Publish\API\Repository\Repository $repository
  77.      * @param \eZ\Publish\SPI\Persistence\Handler $handler
  78.      * @param \eZ\Publish\Core\Repository\Mapper\ContentDomainMapper $contentDomainMapper
  79.      * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
  80.      * @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver
  81.      * @param \eZ\Publish\API\Repository\ContentTypeService $contentTypeService
  82.      * @param array $settings
  83.      * @param \Psr\Log\LoggerInterface|null $logger
  84.      */
  85.     public function __construct(
  86.         RepositoryInterface $repository,
  87.         Handler $handler,
  88.         ContentDomainMapper $contentDomainMapper,
  89.         Helper\NameSchemaService $nameSchemaService,
  90.         PermissionCriterionResolver $permissionCriterionResolver,
  91.         PermissionResolver $permissionResolver,
  92.         LocationFilteringHandler $locationFilteringHandler,
  93.         ContentTypeService $contentTypeService,
  94.         array $settings = [],
  95.         LoggerInterface $logger null
  96.     ) {
  97.         $this->repository $repository;
  98.         $this->persistenceHandler $handler;
  99.         $this->contentDomainMapper $contentDomainMapper;
  100.         $this->nameSchemaService $nameSchemaService;
  101.         $this->permissionResolver $permissionResolver;
  102.         $this->locationFilteringHandler $locationFilteringHandler;
  103.         // Union makes sure default settings are ignored if provided in argument
  104.         $this->settings $settings + [
  105.             //'defaultSetting' => array(),
  106.         ];
  107.         $this->permissionCriterionResolver $permissionCriterionResolver;
  108.         $this->contentTypeService $contentTypeService;
  109.         $this->logger null !== $logger $logger : new NullLogger();
  110.     }
  111.     /**
  112.      * Copies the subtree starting from $subtree as a new subtree of $targetLocation.
  113.      *
  114.      * Only the items on which the user has read access are copied.
  115.      *
  116.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed copy the subtree to the given parent location
  117.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user does not have read access to the whole source subtree
  118.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the target location is a sub location of the given location
  119.      *
  120.      * @param \eZ\Publish\API\Repository\Values\Content\Location $subtree - the subtree denoted by the location to copy
  121.      * @param \eZ\Publish\API\Repository\Values\Content\Location $targetParentLocation - the target parent location for the copy operation
  122.      *
  123.      * @return \eZ\Publish\API\Repository\Values\Content\Location The newly created location of the copied subtree
  124.      */
  125.     public function copySubtree(APILocation $subtreeAPILocation $targetParentLocation): APILocation
  126.     {
  127.         $loadedSubtree $this->loadLocation($subtree->id);
  128.         $loadedTargetLocation $this->loadLocation($targetParentLocation->id);
  129.         if (stripos($loadedTargetLocation->pathString$loadedSubtree->pathString) !== false) {
  130.             throw new InvalidArgumentException('targetParentLocation''Cannot copy subtree to its own descendant Location');
  131.         }
  132.         // check create permission on target
  133.         if (!$this->permissionResolver->canUser('content''create'$loadedSubtree->getContentInfo(), [$loadedTargetLocation])) {
  134.             throw new UnauthorizedException('content''create', ['locationId' => $loadedTargetLocation->id]);
  135.         }
  136.         // Check read access to whole source subtree
  137.         $contentReadCriterion $this->permissionCriterionResolver->getPermissionsCriterion('content''read');
  138.         if ($contentReadCriterion === false) {
  139.             throw new UnauthorizedException('content''read');
  140.         } elseif ($contentReadCriterion !== true) {
  141.             // Query if there are any content in subtree current user don't have access to
  142.             $query = new Query(
  143.                 [
  144.                     'limit' => 0,
  145.                     'filter' => new CriterionLogicalAnd(
  146.                         [
  147.                             new CriterionSubtree($loadedSubtree->pathString),
  148.                             new CriterionLogicalNot($contentReadCriterion),
  149.                         ]
  150.                     ),
  151.                 ]
  152.             );
  153.             $result $this->repository->getSearchService()->findContent($query, [], false);
  154.             if ($result->totalCount 0) {
  155.                 throw new UnauthorizedException('content''read');
  156.             }
  157.         }
  158.         $this->repository->beginTransaction();
  159.         try {
  160.             $newLocation $this->persistenceHandler->locationHandler()->copySubtree(
  161.                 $loadedSubtree->id,
  162.                 $loadedTargetLocation->id,
  163.                 $this->repository->getPermissionResolver()->getCurrentUserReference()->getUserId()
  164.             );
  165.             $content $this->repository->getContentService()->loadContent($newLocation->contentId);
  166.             $urlAliasNames $this->nameSchemaService->resolveUrlAliasSchema($content);
  167.             foreach ($urlAliasNames as $languageCode => $name) {
  168.                 $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
  169.                     $newLocation->id,
  170.                     $loadedTargetLocation->id,
  171.                     $name,
  172.                     $languageCode,
  173.                     $content->contentInfo->alwaysAvailable
  174.                 );
  175.             }
  176.             $this->persistenceHandler->urlAliasHandler()->locationCopied(
  177.                 $loadedSubtree->id,
  178.                 $newLocation->id,
  179.                 $loadedTargetLocation->id
  180.             );
  181.             $this->repository->commit();
  182.         } catch (Exception $e) {
  183.             $this->repository->rollback();
  184.             throw $e;
  185.         }
  186.         return $this->contentDomainMapper->buildLocationWithContent($newLocation$content);
  187.     }
  188.     /**
  189.      * {@inheritdoc}
  190.      */
  191.     public function loadLocation(int $locationId, ?array $prioritizedLanguages null, ?bool $useAlwaysAvailable null): APILocation
  192.     {
  193.         $spiLocation $this->persistenceHandler->locationHandler()->load($locationId$prioritizedLanguages$useAlwaysAvailable ?? true);
  194.         $location $this->contentDomainMapper->buildLocation($spiLocation$prioritizedLanguages ?: [], $useAlwaysAvailable ?? true);
  195.         if (!$this->permissionResolver->canUser('content''read'$location->getContentInfo(), [$location])) {
  196.             throw new UnauthorizedException('content''read', ['locationId' => $location->id]);
  197.         }
  198.         return $location;
  199.     }
  200.     /**
  201.      * {@inheritdoc}
  202.      */
  203.     public function loadLocationList(array $locationIds, ?array $prioritizedLanguages null, ?bool $useAlwaysAvailable null): iterable
  204.     {
  205.         $spiLocations $this->persistenceHandler->locationHandler()->loadList(
  206.             $locationIds,
  207.             $prioritizedLanguages,
  208.             $useAlwaysAvailable ?? true
  209.         );
  210.         if (empty($spiLocations)) {
  211.             return [];
  212.         }
  213.         // Get content id's
  214.         $contentIds = [];
  215.         foreach ($spiLocations as $spiLocation) {
  216.             $contentIds[] = $spiLocation->contentId;
  217.         }
  218.         // Load content info and Get content proxy
  219.         $spiContentInfoList $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
  220.         $contentProxyList $this->contentDomainMapper->buildContentProxyList(
  221.             $spiContentInfoList,
  222.             $prioritizedLanguages ?? [],
  223.             $useAlwaysAvailable ?? true
  224.         );
  225.         // Build locations using the bulk retrieved content info and bulk lazy loaded content proxies.
  226.         $locations = [];
  227.         $permissionResolver $this->repository->getPermissionResolver();
  228.         foreach ($spiLocations as $spiLocation) {
  229.             $location $this->contentDomainMapper->buildLocationWithContent(
  230.                 $spiLocation,
  231.                 $contentProxyList[$spiLocation->contentId] ?? null,
  232.                 $spiContentInfoList[$spiLocation->contentId] ?? null
  233.             );
  234.             if ($permissionResolver->canUser('content''read'$location->getContentInfo(), [$location])) {
  235.                 $locations[$spiLocation->id] = $location;
  236.             }
  237.         }
  238.         return $locations;
  239.     }
  240.     /**
  241.      * {@inheritdoc}
  242.      */
  243.     public function loadLocationByRemoteId(string $remoteId, ?array $prioritizedLanguages null, ?bool $useAlwaysAvailable null): APILocation
  244.     {
  245.         $spiLocation $this->persistenceHandler->locationHandler()->loadByRemoteId($remoteId$prioritizedLanguages$useAlwaysAvailable ?? true);
  246.         $location $this->contentDomainMapper->buildLocation($spiLocation$prioritizedLanguages ?: [], $useAlwaysAvailable ?? true);
  247.         if (!$this->permissionResolver->canUser('content''read'$location->getContentInfo(), [$location])) {
  248.             throw new UnauthorizedException('content''read', ['locationId' => $location->id]);
  249.         }
  250.         return $location;
  251.     }
  252.     /**
  253.      * {@inheritdoc}
  254.      */
  255.     public function loadLocations(ContentInfo $contentInfo, ?APILocation $rootLocation null, ?array $prioritizedLanguages null): iterable
  256.     {
  257.         if (!$contentInfo->published) {
  258.             throw new BadStateException('$contentInfo''The Content item has no published versions');
  259.         }
  260.         $spiLocations $this->persistenceHandler->locationHandler()->loadLocationsByContent(
  261.             $contentInfo->id,
  262.             $rootLocation !== null $rootLocation->id null
  263.         );
  264.         $locations = [];
  265.         $spiInfo $this->persistenceHandler->contentHandler()->loadContentInfo($contentInfo->id);
  266.         $content $this->contentDomainMapper->buildContentProxy($spiInfo$prioritizedLanguages ?: []);
  267.         foreach ($spiLocations as $spiLocation) {
  268.             $location $this->contentDomainMapper->buildLocationWithContent($spiLocation$content$spiInfo);
  269.             if ($this->permissionResolver->canUser('content''read'$location->getContentInfo(), [$location])) {
  270.                 $locations[] = $location;
  271.             }
  272.         }
  273.         return $locations;
  274.     }
  275.     /**
  276.      * {@inheritdoc}
  277.      */
  278.     public function loadLocationChildren(APILocation $locationint $offset 0int $limit 25, ?array $prioritizedLanguages null): LocationList
  279.     {
  280.         if (!$this->contentDomainMapper->isValidLocationSortField($location->sortField)) {
  281.             throw new InvalidArgumentValue('sortField'$location->sortField'Location');
  282.         }
  283.         if (!$this->contentDomainMapper->isValidLocationSortOrder($location->sortOrder)) {
  284.             throw new InvalidArgumentValue('sortOrder'$location->sortOrder'Location');
  285.         }
  286.         if (!is_int($offset)) {
  287.             throw new InvalidArgumentValue('offset'$offset);
  288.         }
  289.         if (!is_int($limit)) {
  290.             throw new InvalidArgumentValue('limit'$limit);
  291.         }
  292.         $childLocations = [];
  293.         $searchResult $this->searchChildrenLocations($location$offset$limit$prioritizedLanguages ?: []);
  294.         foreach ($searchResult->searchHits as $searchHit) {
  295.             $childLocations[] = $searchHit->valueObject;
  296.         }
  297.         return new LocationList(
  298.             [
  299.                 'locations' => $childLocations,
  300.                 'totalCount' => $searchResult->totalCount,
  301.             ]
  302.         );
  303.     }
  304.     /**
  305.      * {@inheritdoc}
  306.      */
  307.     public function loadParentLocationsForDraftContent(VersionInfo $versionInfo, ?array $prioritizedLanguages null): iterable
  308.     {
  309.         if (!$versionInfo->isDraft()) {
  310.             throw new BadStateException(
  311.                 '$contentInfo',
  312.                 sprintf(
  313.                     'Content item [%d] %s is already published. Use LocationService::loadLocations instead.',
  314.                     $versionInfo->contentInfo->id,
  315.                     $versionInfo->contentInfo->name
  316.                 )
  317.             );
  318.         }
  319.         $spiLocations $this->persistenceHandler
  320.             ->locationHandler()
  321.             ->loadParentLocationsForDraftContent($versionInfo->contentInfo->id);
  322.         $contentIds = [];
  323.         foreach ($spiLocations as $spiLocation) {
  324.             $contentIds[] = $spiLocation->contentId;
  325.         }
  326.         $locations = [];
  327.         $permissionResolver $this->repository->getPermissionResolver();
  328.         $spiContentInfoList $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
  329.         $contentList $this->contentDomainMapper->buildContentProxyList($spiContentInfoList$prioritizedLanguages ?: []);
  330.         foreach ($spiLocations as $spiLocation) {
  331.             $location $this->contentDomainMapper->buildLocationWithContent(
  332.                 $spiLocation,
  333.                 $contentList[$spiLocation->contentId],
  334.                 $spiContentInfoList[$spiLocation->contentId]
  335.             );
  336.             if ($permissionResolver->canUser('content''read'$location->getContentInfo(), [$location])) {
  337.                 $locations[] = $location;
  338.             }
  339.         }
  340.         return $locations;
  341.     }
  342.     /**
  343.      * Returns the number of children which are readable by the current user of a location object.
  344.      *
  345.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location
  346.      *
  347.      * @return int
  348.      */
  349.     public function getLocationChildCount(APILocation $location): int
  350.     {
  351.         $searchResult $this->searchChildrenLocations($location00);
  352.         return $searchResult->totalCount;
  353.     }
  354.     /**
  355.      * Searches children locations of the provided parent location id.
  356.      *
  357.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location
  358.      * @param int $offset
  359.      * @param int $limit
  360.      *
  361.      * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
  362.      */
  363.     protected function searchChildrenLocations(APILocation $location$offset 0$limit = -1, array $prioritizedLanguages null)
  364.     {
  365.         $query = new LocationQuery([
  366.             'filter' => new Criterion\ParentLocationId($location->id),
  367.             'offset' => $offset >= ? (int)$offset 0,
  368.             'limit' => $limit >= ? (int)$limit null,
  369.             'sortClauses' => $location->getSortClauses(),
  370.         ]);
  371.         return $this->repository->getSearchService()->findLocations($query, ['languages' => $prioritizedLanguages]);
  372.     }
  373.     /**
  374.      * Creates the new $location in the content repository for the given content.
  375.      *
  376.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to create this location
  377.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the content is already below the specified parent
  378.      *                                        or the parent is a sub location of the location of the content
  379.      *                                        or if set the remoteId exists already
  380.      *
  381.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  382.      * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $locationCreateStruct
  383.      *
  384.      * @return \eZ\Publish\API\Repository\Values\Content\Location the newly created Location
  385.      */
  386.     public function createLocation(ContentInfo $contentInfoLocationCreateStruct $locationCreateStruct): APILocation
  387.     {
  388.         $content $this->contentDomainMapper->buildContentDomainObjectFromPersistence(
  389.             $this->persistenceHandler->contentHandler()->load($contentInfo->id),
  390.             $this->persistenceHandler->contentTypeHandler()->load($contentInfo->contentTypeId)
  391.         );
  392.         $parentLocation $this->contentDomainMapper->buildLocation(
  393.             $this->persistenceHandler->locationHandler()->load($locationCreateStruct->parentLocationId)
  394.         );
  395.         $contentType $content->getContentType();
  396.         $locationCreateStruct->sortField $locationCreateStruct->sortField
  397.             ?? ($contentType->defaultSortField ?? Location::SORT_FIELD_NAME);
  398.         $locationCreateStruct->sortOrder $locationCreateStruct->sortOrder
  399.             ?? ($contentType->defaultSortOrder ?? Location::SORT_ORDER_ASC);
  400.         $contentInfo $content->contentInfo;
  401.         if (!$this->permissionResolver->canUser('content''manage_locations'$contentInfo, [$parentLocation])) {
  402.             throw new UnauthorizedException('content''manage_locations', ['contentId' => $contentInfo->id]);
  403.         }
  404.         if (!$this->permissionResolver->canUser('content''create'$contentInfo, [$parentLocation])) {
  405.             throw new UnauthorizedException('content''create', ['locationId' => $parentLocation->id]);
  406.         }
  407.         // Check if the parent is a sub location of one of the existing content locations (this also solves the
  408.         // situation where parent location actually one of the content locations),
  409.         // or if the content already has location below given location create struct parent
  410.         $existingContentLocations $this->loadLocations($contentInfo);
  411.         if (!empty($existingContentLocations)) {
  412.             foreach ($existingContentLocations as $existingContentLocation) {
  413.                 if (stripos($parentLocation->pathString$existingContentLocation->pathString) !== false) {
  414.                     throw new InvalidArgumentException(
  415.                         '$locationCreateStruct',
  416.                         'Specified parent is a descendant of one of the existing Locations of this content.'
  417.                     );
  418.                 }
  419.                 if ($parentLocation->id == $existingContentLocation->parentLocationId) {
  420.                     throw new InvalidArgumentException(
  421.                         '$locationCreateStruct',
  422.                         'Content is already below the specified parent.'
  423.                     );
  424.                 }
  425.             }
  426.         }
  427.         $spiLocationCreateStruct $this->contentDomainMapper->buildSPILocationCreateStruct(
  428.             $locationCreateStruct,
  429.             $parentLocation,
  430.             $contentInfo->mainLocationId ?? true,
  431.             $contentInfo->id,
  432.             $contentInfo->currentVersionNo,
  433.             $contentInfo->isHidden
  434.         );
  435.         $this->repository->beginTransaction();
  436.         try {
  437.             $newLocation $this->persistenceHandler->locationHandler()->create($spiLocationCreateStruct);
  438.             $urlAliasNames $this->nameSchemaService->resolveUrlAliasSchema($content);
  439.             foreach ($urlAliasNames as $languageCode => $name) {
  440.                 $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
  441.                     $newLocation->id,
  442.                     $newLocation->parentId,
  443.                     $name,
  444.                     $languageCode,
  445.                     $contentInfo->alwaysAvailable,
  446.                     // @todo: this is legacy storage specific for updating ezcontentobject_tree.path_identification_string, to be removed
  447.                     $languageCode === $contentInfo->mainLanguageCode
  448.                 );
  449.             }
  450.             $this->repository->commit();
  451.         } catch (Exception $e) {
  452.             $this->repository->rollback();
  453.             throw $e;
  454.         }
  455.         return $this->contentDomainMapper->buildLocationWithContent($newLocation$content);
  456.     }
  457.     /**
  458.      * Updates $location in the content repository.
  459.      *
  460.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to update this location
  461.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException   if if set the remoteId exists already
  462.      *
  463.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location
  464.      * @param \eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct $locationUpdateStruct
  465.      *
  466.      * @return \eZ\Publish\API\Repository\Values\Content\Location the updated Location
  467.      */
  468.     public function updateLocation(APILocation $locationLocationUpdateStruct $locationUpdateStruct): APILocation
  469.     {
  470.         if (!$this->contentDomainMapper->isValidLocationPriority($locationUpdateStruct->priority)) {
  471.             throw new InvalidArgumentValue('priority'$locationUpdateStruct->priority'LocationUpdateStruct');
  472.         }
  473.         if ($locationUpdateStruct->remoteId !== null && (!is_string($locationUpdateStruct->remoteId) || empty($locationUpdateStruct->remoteId))) {
  474.             throw new InvalidArgumentValue('remoteId'$locationUpdateStruct->remoteId'LocationUpdateStruct');
  475.         }
  476.         if ($locationUpdateStruct->sortField !== null && !$this->contentDomainMapper->isValidLocationSortField($locationUpdateStruct->sortField)) {
  477.             throw new InvalidArgumentValue('sortField'$locationUpdateStruct->sortField'LocationUpdateStruct');
  478.         }
  479.         if ($locationUpdateStruct->sortOrder !== null && !$this->contentDomainMapper->isValidLocationSortOrder($locationUpdateStruct->sortOrder)) {
  480.             throw new InvalidArgumentValue('sortOrder'$locationUpdateStruct->sortOrder'LocationUpdateStruct');
  481.         }
  482.         $loadedLocation $this->loadLocation($location->id);
  483.         if ($locationUpdateStruct->remoteId !== null) {
  484.             try {
  485.                 $existingLocation $this->loadLocationByRemoteId($locationUpdateStruct->remoteId);
  486.                 if ($existingLocation !== null && $existingLocation->id !== $loadedLocation->id) {
  487.                     throw new InvalidArgumentException('locationUpdateStruct''Location with the provided remote ID already exists');
  488.                 }
  489.             } catch (APINotFoundException $e) {
  490.             }
  491.         }
  492.         if (!$this->permissionResolver->canUser('content''edit'$loadedLocation->getContentInfo(), [$loadedLocation])) {
  493.             throw new UnauthorizedException('content''edit', ['locationId' => $loadedLocation->id]);
  494.         }
  495.         $updateStruct = new UpdateStruct();
  496.         $updateStruct->priority $locationUpdateStruct->priority !== null $locationUpdateStruct->priority $loadedLocation->priority;
  497.         $updateStruct->remoteId $locationUpdateStruct->remoteId !== null trim($locationUpdateStruct->remoteId) : $loadedLocation->remoteId;
  498.         $updateStruct->sortField $locationUpdateStruct->sortField !== null $locationUpdateStruct->sortField $loadedLocation->sortField;
  499.         $updateStruct->sortOrder $locationUpdateStruct->sortOrder !== null $locationUpdateStruct->sortOrder $loadedLocation->sortOrder;
  500.         $this->repository->beginTransaction();
  501.         try {
  502.             $this->persistenceHandler->locationHandler()->update($updateStruct$loadedLocation->id);
  503.             $this->repository->commit();
  504.         } catch (Exception $e) {
  505.             $this->repository->rollback();
  506.             throw $e;
  507.         }
  508.         return $this->loadLocation($loadedLocation->id);
  509.     }
  510.     /**
  511.      * Swaps the contents held by $location1 and $location2.
  512.      *
  513.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to swap content
  514.      *
  515.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location1
  516.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location2
  517.      */
  518.     public function swapLocation(APILocation $location1APILocation $location2): void
  519.     {
  520.         $loadedLocation1 $this->loadLocation($location1->id);
  521.         $loadedLocation2 $this->loadLocation($location2->id);
  522.         if (!$this->permissionResolver->canUser('content''edit'$loadedLocation1->getContentInfo(), [$loadedLocation1])) {
  523.             throw new UnauthorizedException('content''edit', ['locationId' => $loadedLocation1->id]);
  524.         }
  525.         if (!$this->permissionResolver->canUser('content''edit'$loadedLocation2->getContentInfo(), [$loadedLocation2])) {
  526.             throw new UnauthorizedException('content''edit', ['locationId' => $loadedLocation2->id]);
  527.         }
  528.         $this->repository->beginTransaction();
  529.         try {
  530.             $this->persistenceHandler->locationHandler()->swap($loadedLocation1->id$loadedLocation2->id);
  531.             $this->persistenceHandler->urlAliasHandler()->locationSwapped(
  532.                 $location1->id,
  533.                 $location1->parentLocationId,
  534.                 $location2->id,
  535.                 $location2->parentLocationId
  536.             );
  537.             $this->persistenceHandler->bookmarkHandler()->locationSwapped($loadedLocation1->id$loadedLocation2->id);
  538.             $this->repository->commit();
  539.         } catch (Exception $e) {
  540.             $this->repository->rollback();
  541.             throw $e;
  542.         }
  543.     }
  544.     /**
  545.      * Hides the $location and marks invisible all descendants of $location.
  546.      *
  547.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to hide this location
  548.      *
  549.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location
  550.      *
  551.      * @return \eZ\Publish\API\Repository\Values\Content\Location $location, with updated hidden value
  552.      */
  553.     public function hideLocation(APILocation $location): APILocation
  554.     {
  555.         if (!$this->permissionResolver->canUser('content''hide'$location->getContentInfo(), [$location])) {
  556.             throw new UnauthorizedException('content''hide', ['locationId' => $location->id]);
  557.         }
  558.         $this->repository->beginTransaction();
  559.         try {
  560.             $this->persistenceHandler->locationHandler()->hide($location->id);
  561.             $this->repository->commit();
  562.         } catch (Exception $e) {
  563.             $this->repository->rollback();
  564.             throw $e;
  565.         }
  566.         return $this->loadLocation($location->id);
  567.     }
  568.     /**
  569.      * Unhides the $location.
  570.      *
  571.      * This method and marks visible all descendants of $locations
  572.      * until a hidden location is found.
  573.      *
  574.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to unhide this location
  575.      *
  576.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location
  577.      *
  578.      * @return \eZ\Publish\API\Repository\Values\Content\Location $location, with updated hidden value
  579.      */
  580.     public function unhideLocation(APILocation $location): APILocation
  581.     {
  582.         if (!$this->permissionResolver->canUser('content''hide'$location->getContentInfo(), [$location])) {
  583.             throw new UnauthorizedException('content''hide', ['locationId' => $location->id]);
  584.         }
  585.         $this->repository->beginTransaction();
  586.         try {
  587.             $this->persistenceHandler->locationHandler()->unHide($location->id);
  588.             $this->repository->commit();
  589.         } catch (Exception $e) {
  590.             $this->repository->rollback();
  591.             throw $e;
  592.         }
  593.         return $this->loadLocation($location->id);
  594.     }
  595.     /**
  596.      * {@inheritdoc}
  597.      */
  598.     public function moveSubtree(APILocation $locationAPILocation $newParentLocation): void
  599.     {
  600.         $location $this->loadLocation($location->id);
  601.         $newParentLocation $this->loadLocation($newParentLocation->id);
  602.         if ($newParentLocation->id === $location->parentLocationId) {
  603.             throw new InvalidArgumentException(
  604.                 '$newParentLocation''new parent Location is the same as the current one'
  605.             );
  606.         }
  607.         if (strpos($newParentLocation->pathString$location->pathString) === 0) {
  608.             throw new InvalidArgumentException(
  609.                 '$newParentLocation',
  610.                 'new parent Location is a descendant of the given $location'
  611.             );
  612.         }
  613.         $contentTypeId $newParentLocation->contentInfo->contentTypeId;
  614.         if (!$this->contentTypeService->loadContentType($contentTypeId)->isContainer) {
  615.             throw new InvalidArgumentException(
  616.                 '$newParentLocation',
  617.                 'Cannot move Location to a parent that is not a container'
  618.             );
  619.         }
  620.         // check create permission on target location
  621.         if (!$this->permissionResolver->canUser('content''create'$location->getContentInfo(), [$newParentLocation])) {
  622.             throw new UnauthorizedException('content''create', ['locationId' => $newParentLocation->id]);
  623.         }
  624.         // Check read access to whole source subtree
  625.         $contentReadCriterion $this->permissionCriterionResolver->getPermissionsCriterion('content''read');
  626.         if ($contentReadCriterion === false) {
  627.             throw new UnauthorizedException('content''read');
  628.         } elseif ($contentReadCriterion !== true) {
  629.             // Query if there are any content in subtree current user don't have access to
  630.             $query = new Query(
  631.                 [
  632.                     'limit' => 0,
  633.                     'filter' => new CriterionLogicalAnd(
  634.                         [
  635.                             new CriterionSubtree($location->pathString),
  636.                             new CriterionLogicalNot($contentReadCriterion),
  637.                         ]
  638.                     ),
  639.                 ]
  640.             );
  641.             $result $this->repository->getSearchService()->findContent($query, [], false);
  642.             if ($result->totalCount 0) {
  643.                 throw new UnauthorizedException('content''read');
  644.             }
  645.         }
  646.         $this->repository->beginTransaction();
  647.         try {
  648.             $this->persistenceHandler->locationHandler()->move($location->id$newParentLocation->id);
  649.             $content $this->repository->getContentService()->loadContent($location->contentId);
  650.             $urlAliasNames $this->nameSchemaService->resolveUrlAliasSchema($content);
  651.             foreach ($urlAliasNames as $languageCode => $name) {
  652.                 $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
  653.                     $location->id,
  654.                     $newParentLocation->id,
  655.                     $name,
  656.                     $languageCode,
  657.                     $content->contentInfo->alwaysAvailable
  658.                 );
  659.             }
  660.             $this->persistenceHandler->urlAliasHandler()->locationMoved(
  661.                 $location->id,
  662.                 $location->parentLocationId,
  663.                 $newParentLocation->id
  664.             );
  665.             $this->repository->commit();
  666.         } catch (Exception $e) {
  667.             $this->repository->rollback();
  668.             throw $e;
  669.         }
  670.     }
  671.     /**
  672.      * Deletes $location and all its descendants.
  673.      *
  674.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user is not allowed to delete this location or a descendant
  675.      *
  676.      * @param \eZ\Publish\API\Repository\Values\Content\Location $location
  677.      */
  678.     public function deleteLocation(APILocation $location): void
  679.     {
  680.         $location $this->loadLocation($location->id);
  681.         $contentInfo $location->contentInfo;
  682.         $versionInfo $this->persistenceHandler->contentHandler()->loadVersionInfo(
  683.             $contentInfo->id,
  684.             $contentInfo->currentVersionNo
  685.         );
  686.         $translations $versionInfo->languageCodes;
  687.         $target = (new Target\Version())->deleteTranslations($translations);
  688.         if (!$this->permissionResolver->canUser('content''manage_locations'$location->getContentInfo())) {
  689.             throw new UnauthorizedException('content''manage_locations', ['locationId' => $location->id]);
  690.         }
  691.         if (!$this->permissionResolver->canUser('content''remove'$location->getContentInfo(), [$location$target])) {
  692.             throw new UnauthorizedException('content''remove', ['locationId' => $location->id]);
  693.         }
  694.         // Check remove access to descendants
  695.         $contentReadCriterion $this->permissionCriterionResolver->getPermissionsCriterion('content''remove');
  696.         if ($contentReadCriterion === false) {
  697.             throw new UnauthorizedException('content''remove');
  698.         } elseif ($contentReadCriterion !== true) {
  699.             // Query if there are any content in subtree current user don't have access to
  700.             $query = new Query(
  701.                 [
  702.                     'limit' => 0,
  703.                     'filter' => new CriterionLogicalAnd(
  704.                         [
  705.                             new CriterionSubtree($location->pathString),
  706.                             new CriterionLogicalNot($contentReadCriterion),
  707.                         ]
  708.                     ),
  709.                 ]
  710.             );
  711.             $result $this->repository->getSearchService()->findContent($query, [], false);
  712.             if ($result->totalCount 0) {
  713.                 throw new UnauthorizedException('content''remove');
  714.             }
  715.         }
  716.         $this->repository->beginTransaction();
  717.         try {
  718.             $this->persistenceHandler->locationHandler()->removeSubtree($location->id);
  719.             $this->persistenceHandler->urlAliasHandler()->locationDeleted($location->id);
  720.             $this->repository->commit();
  721.         } catch (Exception $e) {
  722.             $this->repository->rollback();
  723.             throw $e;
  724.         }
  725.     }
  726.     /**
  727.      * Instantiates a new location create class.
  728.      *
  729.      * @param mixed $parentLocationId the parent under which the new location should be created
  730.      * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|null $contentType
  731.      *
  732.      * @return \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct
  733.      */
  734.     public function newLocationCreateStruct($parentLocationIdContentType $contentType null): LocationCreateStruct
  735.     {
  736.         $properties = [
  737.             'parentLocationId' => $parentLocationId,
  738.         ];
  739.         if ($contentType) {
  740.             $properties['sortField'] = $contentType->defaultSortField;
  741.             $properties['sortOrder'] = $contentType->defaultSortOrder;
  742.         }
  743.         return new LocationCreateStruct($properties);
  744.     }
  745.     /**
  746.      * Instantiates a new location update class.
  747.      *
  748.      * @return \eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct
  749.      */
  750.     public function newLocationUpdateStruct(): LocationUpdateStruct
  751.     {
  752.         return new LocationUpdateStruct();
  753.     }
  754.     /**
  755.      * Get the total number of all existing Locations. Can be combined with loadAllLocations.
  756.      *
  757.      * @see loadAllLocations
  758.      *
  759.      * @return int Total number of Locations
  760.      */
  761.     public function getAllLocationsCount(): int
  762.     {
  763.         return $this->persistenceHandler->locationHandler()->countAllLocations();
  764.     }
  765.     /**
  766.      * Bulk-load all existing Locations, constrained by $limit and $offset to paginate results.
  767.      *
  768.      * @param int $offset
  769.      * @param int $limit
  770.      *
  771.      * @return \eZ\Publish\API\Repository\Values\Content\Location[]
  772.      *
  773.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
  774.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  775.      */
  776.     public function loadAllLocations(int $offset 0int $limit 25): array
  777.     {
  778.         $spiLocations $this->persistenceHandler->locationHandler()->loadAllLocations(
  779.             $offset,
  780.             $limit
  781.         );
  782.         $contentIds array_unique(
  783.             array_map(
  784.                 function (SPILocation $spiLocation) {
  785.                     return $spiLocation->contentId;
  786.                 },
  787.                 $spiLocations
  788.             )
  789.         );
  790.         $permissionResolver $this->repository->getPermissionResolver();
  791.         $spiContentInfoList $this->persistenceHandler->contentHandler()->loadContentInfoList(
  792.             $contentIds
  793.         );
  794.         $contentList $this->contentDomainMapper->buildContentProxyList(
  795.             $spiContentInfoList,
  796.             Language::ALL,
  797.             false
  798.         );
  799.         $locations = [];
  800.         foreach ($spiLocations as $spiLocation) {
  801.             if (!isset($spiContentInfoList[$spiLocation->contentId], $contentList[$spiLocation->contentId])) {
  802.                 $this->logger->warning(
  803.                     sprintf(
  804.                         'Location %d has missing content %d',
  805.                         $spiLocation->id,
  806.                         $spiLocation->contentId
  807.                     )
  808.                 );
  809.                 continue;
  810.             }
  811.             $location $this->contentDomainMapper->buildLocationWithContent(
  812.                 $spiLocation,
  813.                 $contentList[$spiLocation->contentId],
  814.                 $spiContentInfoList[$spiLocation->contentId]
  815.             );
  816.             $contentInfo $location->getContentInfo();
  817.             if (!$permissionResolver->canUser('content''read'$contentInfo, [$location])) {
  818.                 continue;
  819.             }
  820.             $locations[] = $location;
  821.         }
  822.         return $locations;
  823.     }
  824.     /**
  825.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  826.      */
  827.     public function find(Filter $filter, ?array $languages null): LocationList
  828.     {
  829.         $filter = clone $filter;
  830.         if (!empty($languages)) {
  831.             $filter->andWithCriterion(new LanguageCode($languages));
  832.         }
  833.         $permissionCriterion $this->permissionCriterionResolver->getQueryPermissionsCriterion();
  834.         if ($permissionCriterion instanceof Criterion\MatchNone) {
  835.             return new LocationList();
  836.         }
  837.         if (!$permissionCriterion instanceof Criterion\MatchAll) {
  838.             if (!$permissionCriterion instanceof FilteringCriterion) {
  839.                 return new LocationList();
  840.             }
  841.             $filter->andWithCriterion($permissionCriterion);
  842.         }
  843.         $locations = [];
  844.         foreach ($this->locationFilteringHandler->find($filter) as $locationWithContentInfo) {
  845.             $spiContentInfo $locationWithContentInfo->getContentInfo();
  846.             $locations[] = $this->contentDomainMapper->buildLocationWithContent(
  847.                 $locationWithContentInfo->getLocation(),
  848.                 $this->contentDomainMapper->buildContentProxy($spiContentInfo),
  849.                 $spiContentInfo,
  850.             );
  851.         }
  852.         return new LocationList(
  853.             [
  854.                 'totalCount' => count($locations),
  855.                 'locations' => $locations,
  856.             ]
  857.         );
  858.     }
  859. }