vendor/ezsystems/ezpublish-kernel/eZ/Publish/Core/Repository/LocationService.php line 190

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