vendor/ezsystems/ezplatform-kernel/eZ/Publish/Core/Repository/ContentService.php line 72

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\ContentService as ContentServiceInterface;
  9. use eZ\Publish\API\Repository\PermissionService;
  10. use eZ\Publish\API\Repository\Repository as RepositoryInterface;
  11. use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
  12. use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LanguageCode;
  13. use eZ\Publish\API\Repository\Values\ValueObject;
  14. use eZ\Publish\Core\FieldType\FieldTypeRegistry;
  15. use eZ\Publish\API\Repository\Values\Content\ContentDraftList;
  16. use eZ\Publish\API\Repository\Values\Content\DraftList\Item\ContentDraftListItem;
  17. use eZ\Publish\API\Repository\Values\Content\DraftList\Item\UnauthorizedContentDraftListItem;
  18. use eZ\Publish\API\Repository\Values\Content\RelationList;
  19. use eZ\Publish\API\Repository\Values\Content\RelationList\Item\RelationListItem;
  20. use eZ\Publish\API\Repository\Values\Content\RelationList\Item\UnauthorizedRelationListItem;
  21. use eZ\Publish\API\Repository\Values\User\UserReference;
  22. use eZ\Publish\Core\Repository\Mapper\ContentDomainMapper;
  23. use eZ\Publish\Core\Repository\Mapper\ContentMapper;
  24. use eZ\Publish\Core\Repository\Values\Content\Content;
  25. use eZ\Publish\Core\Repository\Values\Content\Location;
  26. use eZ\Publish\API\Repository\Values\Content\Language;
  27. use eZ\Publish\SPI\Persistence\Filter\Content\Handler as ContentFilteringHandler;
  28. use eZ\Publish\SPI\FieldType\Comparable;
  29. use eZ\Publish\SPI\FieldType\FieldType;
  30. use eZ\Publish\SPI\FieldType\Value;
  31. use eZ\Publish\SPI\Persistence\Handler;
  32. use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
  33. use eZ\Publish\API\Repository\Values\ContentType\ContentType;
  34. use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
  35. use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
  36. use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
  37. use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
  38. use eZ\Publish\API\Repository\Values\Content\ContentInfo;
  39. use eZ\Publish\API\Repository\Values\User\User;
  40. use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
  41. use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
  42. use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
  43. use eZ\Publish\Core\Base\Exceptions\BadStateException;
  44. use eZ\Publish\Core\Base\Exceptions\NotFoundException;
  45. use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
  46. use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
  47. use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
  48. use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
  49. use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
  50. use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
  51. use eZ\Publish\SPI\Limitation\Target;
  52. use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
  53. use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
  54. use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
  55. use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
  56. use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
  57. use eZ\Publish\SPI\Persistence\Content\ContentInfo as SPIContentInfo;
  58. use Exception;
  59. use eZ\Publish\SPI\Repository\Validator\ContentValidator;
  60. use eZ\Publish\API\Repository\Values\Content\ContentList;
  61. use eZ\Publish\API\Repository\Values\Filter\Filter;
  62. use eZ\Publish\SPI\Repository\Values\Filter\FilteringCriterion;
  63. use function count;
  64. use function sprintf;
  65. /**
  66.  * This class provides service methods for managing content.
  67.  */
  68. class ContentService implements ContentServiceInterface
  69. {
  70.     /** @var \eZ\Publish\Core\Repository\Repository */
  71.     protected $repository;
  72.     /** @var \eZ\Publish\SPI\Persistence\Handler */
  73.     protected $persistenceHandler;
  74.     /** @var array */
  75.     protected $settings;
  76.     /** @var \eZ\Publish\Core\Repository\Mapper\ContentDomainMapper */
  77.     protected $contentDomainMapper;
  78.     /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
  79.     protected $relationProcessor;
  80.     /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
  81.     protected $nameSchemaService;
  82.     /** @var \eZ\Publish\Core\FieldType\FieldTypeRegistry */
  83.     protected $fieldTypeRegistry;
  84.     /** @var \eZ\Publish\API\Repository\PermissionResolver */
  85.     private $permissionResolver;
  86.     /** @var \eZ\Publish\Core\Repository\Mapper\ContentMapper */
  87.     private $contentMapper;
  88.     /** @var \eZ\Publish\SPI\Repository\Validator\ContentValidator */
  89.     private $contentValidator;
  90.     /** @var \eZ\Publish\SPI\Persistence\Filter\Content\Handler */
  91.     private $contentFilteringHandler;
  92.     public function __construct(
  93.         RepositoryInterface $repository,
  94.         Handler $handler,
  95.         ContentDomainMapper $contentDomainMapper,
  96.         Helper\RelationProcessor $relationProcessor,
  97.         Helper\NameSchemaService $nameSchemaService,
  98.         FieldTypeRegistry $fieldTypeRegistry,
  99.         PermissionService $permissionService,
  100.         ContentMapper $contentMapper,
  101.         ContentValidator $contentValidator,
  102.         ContentFilteringHandler $contentFilteringHandler,
  103.         array $settings = []
  104.     ) {
  105.         $this->repository $repository;
  106.         $this->persistenceHandler $handler;
  107.         $this->contentDomainMapper $contentDomainMapper;
  108.         $this->relationProcessor $relationProcessor;
  109.         $this->nameSchemaService $nameSchemaService;
  110.         $this->fieldTypeRegistry $fieldTypeRegistry;
  111.         // Union makes sure default settings are ignored if provided in argument
  112.         $this->settings $settings + [
  113.             // Version archive limit (0-50), only enforced on publish, not on un-publish.
  114.             'default_version_archive_limit' => 5,
  115.             'remove_archived_versions_on_publish' => true,
  116.         ];
  117.         $this->contentFilteringHandler $contentFilteringHandler;
  118.         $this->permissionResolver $permissionService;
  119.         $this->contentMapper $contentMapper;
  120.         $this->contentValidator $contentValidator;
  121.     }
  122.     /**
  123.      * Loads a content info object.
  124.      *
  125.      * To load fields use loadContent
  126.      *
  127.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
  128.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
  129.      *
  130.      * @param int $contentId
  131.      *
  132.      * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
  133.      */
  134.     public function loadContentInfo(int $contentId): ContentInfo
  135.     {
  136.         $contentInfo $this->internalLoadContentInfoById($contentId);
  137.         if (!$this->permissionResolver->canUser('content''read'$contentInfo)) {
  138.             throw new UnauthorizedException('content''read', ['contentId' => $contentId]);
  139.         }
  140.         return $contentInfo;
  141.     }
  142.     /**
  143.      * {@inheritdoc}
  144.      */
  145.     public function loadContentInfoList(array $contentIds): iterable
  146.     {
  147.         $contentInfoList = [];
  148.         $spiInfoList $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
  149.         foreach ($spiInfoList as $id => $spiInfo) {
  150.             $contentInfo $this->contentDomainMapper->buildContentInfoDomainObject($spiInfo);
  151.             if ($this->permissionResolver->canUser('content''read'$contentInfo)) {
  152.                 $contentInfoList[$id] = $contentInfo;
  153.             }
  154.         }
  155.         return $contentInfoList;
  156.     }
  157.     /**
  158.      * Loads a content info object.
  159.      *
  160.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
  161.      *
  162.      * @param int $id
  163.      *
  164.      * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
  165.      */
  166.     public function internalLoadContentInfoById(int $id): ContentInfo
  167.     {
  168.         try {
  169.             return $this->contentDomainMapper->buildContentInfoDomainObject(
  170.                 $this->persistenceHandler->contentHandler()->loadContentInfo($id)
  171.             );
  172.         } catch (APINotFoundException $e) {
  173.             throw new NotFoundException('Content'$id$e);
  174.         }
  175.     }
  176.     /**
  177.      * Loads a content info object by remote id.
  178.      *
  179.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
  180.      *
  181.      * @param string $remoteId
  182.      *
  183.      * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
  184.      */
  185.     public function internalLoadContentInfoByRemoteId(string $remoteId): ContentInfo
  186.     {
  187.         try {
  188.             return $this->contentDomainMapper->buildContentInfoDomainObject(
  189.                 $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($remoteId)
  190.             );
  191.         } catch (APINotFoundException $e) {
  192.             throw new NotFoundException('Content'$remoteId$e);
  193.         }
  194.     }
  195.     /**
  196.      * Loads a content info object for the given remoteId.
  197.      *
  198.      * To load fields use loadContent
  199.      *
  200.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
  201.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given remote id does not exist
  202.      *
  203.      * @param string $remoteId
  204.      *
  205.      * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
  206.      */
  207.     public function loadContentInfoByRemoteId(string $remoteId): ContentInfo
  208.     {
  209.         $contentInfo $this->internalLoadContentInfoByRemoteId($remoteId);
  210.         if (!$this->permissionResolver->canUser('content''read'$contentInfo)) {
  211.             throw new UnauthorizedException('content''read', ['remoteId' => $remoteId]);
  212.         }
  213.         return $contentInfo;
  214.     }
  215.     /**
  216.      * Loads a version info of the given content object.
  217.      *
  218.      * If no version number is given, the method returns the current version
  219.      *
  220.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
  221.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
  222.      *
  223.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  224.      * @param int|null $versionNo the version number. If not given the current version is returned.
  225.      *
  226.      * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
  227.      */
  228.     public function loadVersionInfo(ContentInfo $contentInfo, ?int $versionNo null): APIVersionInfo
  229.     {
  230.         return $this->loadVersionInfoById($contentInfo->id$versionNo);
  231.     }
  232.     /**
  233.      * Loads a version info of the given content object id.
  234.      *
  235.      * If no version number is given, the method returns the current version
  236.      *
  237.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
  238.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
  239.      *
  240.      * @param int $contentId
  241.      * @param int|null $versionNo the version number. If not given the current version is returned.
  242.      *
  243.      * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
  244.      */
  245.     public function loadVersionInfoById(int $contentId, ?int $versionNo null): APIVersionInfo
  246.     {
  247.         try {
  248.             $spiVersionInfo $this->persistenceHandler->contentHandler()->loadVersionInfo(
  249.                 $contentId,
  250.                 $versionNo
  251.             );
  252.         } catch (APINotFoundException $e) {
  253.             throw new NotFoundException(
  254.                 'VersionInfo',
  255.                 [
  256.                     'contentId' => $contentId,
  257.                     'versionNo' => $versionNo,
  258.                 ],
  259.                 $e
  260.             );
  261.         }
  262.         $versionInfo $this->contentDomainMapper->buildVersionInfoDomainObject($spiVersionInfo);
  263.         if ($versionInfo->isPublished()) {
  264.             $function 'read';
  265.         } else {
  266.             $function 'versionread';
  267.         }
  268.         if (!$this->permissionResolver->canUser('content'$function$versionInfo)) {
  269.             throw new UnauthorizedException('content'$function, ['contentId' => $contentId]);
  270.         }
  271.         return $versionInfo;
  272.     }
  273.     /**
  274.      * {@inheritdoc}
  275.      */
  276.     public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages null, ?int $versionNo nullbool $useAlwaysAvailable true): APIContent
  277.     {
  278.         // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
  279.         if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
  280.             $useAlwaysAvailable false;
  281.         }
  282.         return $this->loadContent(
  283.             $contentInfo->id,
  284.             $languages,
  285.             $versionNo,// On purpose pass as-is and not use $contentInfo, to make sure to return actual current version on null
  286.             $useAlwaysAvailable
  287.         );
  288.     }
  289.     /**
  290.      * {@inheritdoc}
  291.      */
  292.     public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages nullbool $useAlwaysAvailable true): APIContent
  293.     {
  294.         // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
  295.         if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
  296.             $useAlwaysAvailable false;
  297.         }
  298.         return $this->loadContent(
  299.             $versionInfo->getContentInfo()->id,
  300.             $languages,
  301.             $versionInfo->versionNo,
  302.             $useAlwaysAvailable
  303.         );
  304.     }
  305.     /**
  306.      * {@inheritdoc}
  307.      */
  308.     public function loadContent(int $contentId, array $languages null, ?int $versionNo nullbool $useAlwaysAvailable true): APIContent
  309.     {
  310.         $content $this->internalLoadContentById($contentId$languages$versionNo$useAlwaysAvailable);
  311.         if (!$this->permissionResolver->canUser('content''read'$content)) {
  312.             throw new UnauthorizedException('content''read', ['contentId' => $contentId]);
  313.         }
  314.         if (
  315.             !$content->getVersionInfo()->isPublished()
  316.             && !$this->permissionResolver->canUser('content''versionread'$content)
  317.         ) {
  318.             throw new UnauthorizedException('content''versionread', ['contentId' => $contentId'versionNo' => $versionNo]);
  319.         }
  320.         return $content;
  321.     }
  322.     public function internalLoadContentById(
  323.         int $id,
  324.         ?array $languages null,
  325.         int $versionNo null,
  326.         bool $useAlwaysAvailable true
  327.     ): APIContent {
  328.         try {
  329.             $spiContentInfo $this->persistenceHandler->contentHandler()->loadContentInfo($id);
  330.             return $this->internalLoadContentBySPIContentInfo(
  331.                 $spiContentInfo,
  332.                 $languages,
  333.                 $versionNo,
  334.                 $useAlwaysAvailable
  335.             );
  336.         } catch (APINotFoundException $e) {
  337.             throw new NotFoundException(
  338.                 'Content',
  339.                 [
  340.                     'id' => $id,
  341.                     'languages' => $languages,
  342.                     'versionNo' => $versionNo,
  343.                 ],
  344.                 $e
  345.             );
  346.         }
  347.     }
  348.     public function internalLoadContentByRemoteId(
  349.         string $remoteId,
  350.         array $languages null,
  351.         int $versionNo null,
  352.         bool $useAlwaysAvailable true
  353.     ): APIContent {
  354.         try {
  355.             $spiContentInfo $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($remoteId);
  356.             return $this->internalLoadContentBySPIContentInfo(
  357.                 $spiContentInfo,
  358.                 $languages,
  359.                 $versionNo,
  360.                 $useAlwaysAvailable
  361.             );
  362.         } catch (APINotFoundException $e) {
  363.             throw new NotFoundException(
  364.                 'Content',
  365.                 [
  366.                     'remoteId' => $remoteId,
  367.                     'languages' => $languages,
  368.                     'versionNo' => $versionNo,
  369.                 ],
  370.                 $e
  371.             );
  372.         }
  373.     }
  374.     private function internalLoadContentBySPIContentInfo(SPIContentInfo $spiContentInfo, array $languages nullint $versionNo nullbool $useAlwaysAvailable true): APIContent
  375.     {
  376.         $loadLanguages $languages;
  377.         $alwaysAvailableLanguageCode null;
  378.         // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
  379.         // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
  380.         if (!empty($loadLanguages) && $useAlwaysAvailable && $spiContentInfo->alwaysAvailable) {
  381.             $loadLanguages[] = $alwaysAvailableLanguageCode $spiContentInfo->mainLanguageCode;
  382.             $loadLanguages array_unique($loadLanguages);
  383.         }
  384.         $spiContent $this->persistenceHandler->contentHandler()->load(
  385.             $spiContentInfo->id,
  386.             $versionNo,
  387.             $loadLanguages
  388.         );
  389.         if ($languages === null) {
  390.             $languages = [];
  391.         }
  392.         return $this->contentDomainMapper->buildContentDomainObject(
  393.             $spiContent,
  394.             $this->repository->getContentTypeService()->loadContentType(
  395.                 $spiContent->versionInfo->contentInfo->contentTypeId,
  396.                 $languages
  397.             ),
  398.             $languages,
  399.             $alwaysAvailableLanguageCode
  400.         );
  401.     }
  402.     /**
  403.      * Loads content in a version for the content object reference by the given remote id.
  404.      *
  405.      * If no version is given, the method returns the current version
  406.      *
  407.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content or version with the given remote id does not exist
  408.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the user has no access to read content and in case of un-published content: read versions
  409.      *
  410.      * @param string $remoteId
  411.      * @param array $languages A language filter for fields. If not given all languages are returned
  412.      * @param int $versionNo the version number. If not given the current version is returned
  413.      * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
  414.      *
  415.      * @return \eZ\Publish\API\Repository\Values\Content\Content
  416.      */
  417.     public function loadContentByRemoteId(string $remoteId, array $languages null, ?int $versionNo nullbool $useAlwaysAvailable true): APIContent
  418.     {
  419.         $content $this->internalLoadContentByRemoteId($remoteId$languages$versionNo$useAlwaysAvailable);
  420.         if (!$this->permissionResolver->canUser('content''read'$content)) {
  421.             throw new UnauthorizedException('content''read', ['remoteId' => $remoteId]);
  422.         }
  423.         if (
  424.             !$content->getVersionInfo()->isPublished()
  425.             && !$this->permissionResolver->canUser('content''versionread'$content)
  426.         ) {
  427.             throw new UnauthorizedException('content''versionread', ['remoteId' => $remoteId'versionNo' => $versionNo]);
  428.         }
  429.         return $content;
  430.     }
  431.     /**
  432.      * Bulk-load Content items by the list of ContentInfo Value Objects.
  433.      *
  434.      * Note: it does not throw exceptions on load, just ignores erroneous Content item.
  435.      * Moreover, since the method works on pre-loaded ContentInfo list, it is assumed that user is
  436.      * allowed to access every Content on the list.
  437.      *
  438.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo[] $contentInfoList
  439.      * @param string[] $languages A language priority, filters returned fields and is used as prioritized language code on
  440.      *                            returned value object. If not given all languages are returned.
  441.      * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true,
  442.      *                                 unless all languages have been asked for.
  443.      *
  444.      * @return \eZ\Publish\API\Repository\Values\Content\Content[] list of Content items with Content Ids as keys
  445.      */
  446.     public function loadContentListByContentInfo(
  447.         array $contentInfoList,
  448.         array $languages = [],
  449.         bool $useAlwaysAvailable true
  450.     ): iterable {
  451.         $loadAllLanguages $languages === Language::ALL;
  452.         $contentIds = [];
  453.         $contentTypeIds = [];
  454.         $translations $languages;
  455.         foreach ($contentInfoList as $contentInfo) {
  456.             $contentIds[] = $contentInfo->id;
  457.             $contentTypeIds[] = $contentInfo->contentTypeId;
  458.             // Unless we are told to load all languages, we add main language to translations so they are loaded too
  459.             // Might in some case load more languages then intended, but prioritised handling will pick right one
  460.             if (!$loadAllLanguages && $useAlwaysAvailable && $contentInfo->alwaysAvailable) {
  461.                 $translations[] = $contentInfo->mainLanguageCode;
  462.             }
  463.         }
  464.         $contentList = [];
  465.         $translations array_unique($translations);
  466.         $spiContentList $this->persistenceHandler->contentHandler()->loadContentList(
  467.             $contentIds,
  468.             $translations
  469.         );
  470.         $contentTypeList $this->repository->getContentTypeService()->loadContentTypeList(
  471.             array_unique($contentTypeIds),
  472.             $languages
  473.         );
  474.         foreach ($spiContentList as $contentId => $spiContent) {
  475.             $contentInfo $spiContent->versionInfo->contentInfo;
  476.             $contentList[$contentId] = $this->contentDomainMapper->buildContentDomainObject(
  477.                 $spiContent,
  478.                 $contentTypeList[$contentInfo->contentTypeId],
  479.                 $languages,
  480.                 $contentInfo->alwaysAvailable $contentInfo->mainLanguageCode null
  481.             );
  482.         }
  483.         return $contentList;
  484.     }
  485.     /**
  486.      * Creates a new content draft assigned to the authenticated user.
  487.      *
  488.      * If a different userId is given in $contentCreateStruct it is assigned to the given user
  489.      * but this required special rights for the authenticated user
  490.      * (this is useful for content staging where the transfer process does not
  491.      * have to authenticate with the user which created the content object in the source server).
  492.      * The user has to publish the draft if it should be visible.
  493.      * In 4.x at least one location has to be provided in the location creation array.
  494.      *
  495.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to create the content in the given location
  496.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the provided remoteId exists in the system, required properties on
  497.      *                                                                        struct are missing or invalid, or if multiple locations are under the
  498.      *                                                                        same parent.
  499.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
  500.      *                                                                               or if a required field is missing / set to an empty value.
  501.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
  502.      *                                                                          or value is set for non-translatable field in language
  503.      *                                                                          other than main.
  504.      *
  505.      * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
  506.      * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs For each location parent under which a location should be created for the content
  507.      *
  508.      * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
  509.      */
  510.     public function createContent(APIContentCreateStruct $contentCreateStruct, array $locationCreateStructs = [], ?array $fieldIdentifiersToValidate null): APIContent
  511.     {
  512.         if ($contentCreateStruct->mainLanguageCode === null) {
  513.             throw new InvalidArgumentException('$contentCreateStruct'"the 'mainLanguageCode' property must be set");
  514.         }
  515.         if ($contentCreateStruct->contentType === null) {
  516.             throw new InvalidArgumentException('$contentCreateStruct'"the 'contentType' property must be set");
  517.         }
  518.         $contentCreateStruct = clone $contentCreateStruct;
  519.         if ($contentCreateStruct->ownerId === null) {
  520.             $contentCreateStruct->ownerId $this->permissionResolver->getCurrentUserReference()->getUserId();
  521.         }
  522.         if ($contentCreateStruct->alwaysAvailable === null) {
  523.             $contentCreateStruct->alwaysAvailable $contentCreateStruct->contentType->defaultAlwaysAvailable ?: false;
  524.         }
  525.         $contentCreateStruct->contentType $this->repository->getContentTypeService()->loadContentType(
  526.             $contentCreateStruct->contentType->id
  527.         );
  528.         $contentCreateStruct->fields $this->contentMapper->getFieldsForCreate(
  529.             $contentCreateStruct->fields,
  530.             $contentCreateStruct->contentType
  531.         );
  532.         if (empty($contentCreateStruct->sectionId)) {
  533.             if (isset($locationCreateStructs[0])) {
  534.                 $location $this->repository->getLocationService()->loadLocation(
  535.                     $locationCreateStructs[0]->parentLocationId
  536.                 );
  537.                 $contentCreateStruct->sectionId $location->contentInfo->sectionId;
  538.             } else {
  539.                 $contentCreateStruct->sectionId 1;
  540.             }
  541.         }
  542.         if (!$this->permissionResolver->canUser('content''create'$contentCreateStruct$locationCreateStructs)) {
  543.             throw new UnauthorizedException(
  544.                 'content',
  545.                 'create',
  546.                 [
  547.                     'parentLocationId' => isset($locationCreateStructs[0]) ?
  548.                             $locationCreateStructs[0]->parentLocationId :
  549.                             null,
  550.                     'sectionId' => $contentCreateStruct->sectionId,
  551.                 ]
  552.             );
  553.         }
  554.         if (!empty($contentCreateStruct->remoteId)) {
  555.             try {
  556.                 $this->loadContentByRemoteId($contentCreateStruct->remoteId);
  557.                 throw new InvalidArgumentException(
  558.                     '$contentCreateStruct',
  559.                     "Another Content item with remoteId '{$contentCreateStruct->remoteId}' exists"
  560.                 );
  561.             } catch (APINotFoundException $e) {
  562.                 // Do nothing
  563.             }
  564.         } else {
  565.             $contentCreateStruct->remoteId $this->contentDomainMapper->getUniqueHash($contentCreateStruct);
  566.         }
  567.         $errors $this->validate(
  568.             $contentCreateStruct,
  569.             [],
  570.             $fieldIdentifiersToValidate
  571.         );
  572.         if (!empty($errors)) {
  573.             throw new ContentFieldValidationException($errors);
  574.         }
  575.         $spiLocationCreateStructs $spiLocationCreateStructs $this->buildSPILocationCreateStructs(
  576.             $locationCreateStructs,
  577.             $contentCreateStruct->contentType
  578.         );
  579.         $languageCodes $this->contentMapper->getLanguageCodesForCreate($contentCreateStruct);
  580.         $fields $this->contentMapper->mapFieldsForCreate($contentCreateStruct);
  581.         $fieldValues = [];
  582.         $spiFields = [];
  583.         $inputRelations = [];
  584.         $locationIdToContentIdMapping = [];
  585.         foreach ($contentCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
  586.             /** @var $fieldType \eZ\Publish\Core\FieldType\FieldType */
  587.             $fieldType $this->fieldTypeRegistry->getFieldType(
  588.                 $fieldDefinition->fieldTypeIdentifier
  589.             );
  590.             foreach ($languageCodes as $languageCode) {
  591.                 $isEmptyValue false;
  592.                 $valueLanguageCode $fieldDefinition->isTranslatable $languageCode $contentCreateStruct->mainLanguageCode;
  593.                 $isLanguageMain $languageCode === $contentCreateStruct->mainLanguageCode;
  594.                 $fieldValue $this->contentMapper->getFieldValueForCreate(
  595.                     $fieldDefinition,
  596.                     $fields[$fieldDefinition->identifier][$valueLanguageCode] ?? null
  597.                 );
  598.                 if ($fieldType->isEmptyValue($fieldValue)) {
  599.                     $isEmptyValue true;
  600.                 }
  601.                 $this->relationProcessor->appendFieldRelations(
  602.                     $inputRelations,
  603.                     $locationIdToContentIdMapping,
  604.                     $fieldType,
  605.                     $fieldValue,
  606.                     $fieldDefinition->id
  607.                 );
  608.                 $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
  609.                 // Only non-empty value for: translatable field or in main language
  610.                 if (
  611.                     (!$isEmptyValue && $fieldDefinition->isTranslatable) ||
  612.                     (!$isEmptyValue && $isLanguageMain)
  613.                 ) {
  614.                     $spiFields[] = new SPIField(
  615.                         [
  616.                             'id' => null,
  617.                             'fieldDefinitionId' => $fieldDefinition->id,
  618.                             'type' => $fieldDefinition->fieldTypeIdentifier,
  619.                             'value' => $fieldType->toPersistenceValue($fieldValue),
  620.                             'languageCode' => $languageCode,
  621.                             'versionNo' => null,
  622.                         ]
  623.                     );
  624.                 }
  625.             }
  626.         }
  627.         $spiContentCreateStruct = new SPIContentCreateStruct(
  628.             [
  629.                 'name' => $this->nameSchemaService->resolve(
  630.                     $contentCreateStruct->contentType->nameSchema,
  631.                     $contentCreateStruct->contentType,
  632.                     $fieldValues,
  633.                     $languageCodes
  634.                 ),
  635.                 'typeId' => $contentCreateStruct->contentType->id,
  636.                 'sectionId' => $contentCreateStruct->sectionId,
  637.                 'ownerId' => $contentCreateStruct->ownerId,
  638.                 'locations' => $spiLocationCreateStructs,
  639.                 'fields' => $spiFields,
  640.                 'alwaysAvailable' => $contentCreateStruct->alwaysAvailable,
  641.                 'remoteId' => $contentCreateStruct->remoteId,
  642.                 'modified' => isset($contentCreateStruct->modificationDate) ? $contentCreateStruct->modificationDate->getTimestamp() : time(),
  643.                 'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
  644.                     $contentCreateStruct->mainLanguageCode
  645.                 )->id,
  646.             ]
  647.         );
  648.         $defaultObjectStates $this->getDefaultObjectStates();
  649.         $this->repository->beginTransaction();
  650.         try {
  651.             $spiContent $this->persistenceHandler->contentHandler()->create($spiContentCreateStruct);
  652.             $this->relationProcessor->processFieldRelations(
  653.                 $inputRelations,
  654.                 $spiContent->versionInfo->contentInfo->id,
  655.                 $spiContent->versionInfo->versionNo,
  656.                 $contentCreateStruct->contentType
  657.             );
  658.             $objectStateHandler $this->persistenceHandler->objectStateHandler();
  659.             foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
  660.                 $objectStateHandler->setContentState(
  661.                     $spiContent->versionInfo->contentInfo->id,
  662.                     $objectStateGroupId,
  663.                     $objectState->id
  664.                 );
  665.             }
  666.             $this->repository->commit();
  667.         } catch (Exception $e) {
  668.             $this->repository->rollback();
  669.             throw $e;
  670.         }
  671.         return $this->contentDomainMapper->buildContentDomainObject(
  672.             $spiContent,
  673.             $contentCreateStruct->contentType
  674.         );
  675.     }
  676.     /**
  677.      * Returns an array of default content states with content state group id as key.
  678.      *
  679.      * @return \eZ\Publish\SPI\Persistence\Content\ObjectState[]
  680.      */
  681.     protected function getDefaultObjectStates(): array
  682.     {
  683.         $defaultObjectStatesMap = [];
  684.         $objectStateHandler $this->persistenceHandler->objectStateHandler();
  685.         foreach ($objectStateHandler->loadAllGroups() as $objectStateGroup) {
  686.             foreach ($objectStateHandler->loadObjectStates($objectStateGroup->id) as $objectState) {
  687.                 // Only register the first object state which is the default one.
  688.                 $defaultObjectStatesMap[$objectStateGroup->id] = $objectState;
  689.                 break;
  690.             }
  691.         }
  692.         return $defaultObjectStatesMap;
  693.     }
  694.     /**
  695.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  696.      *
  697.      * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs
  698.      * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|null $contentType
  699.      *
  700.      * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct[]
  701.      */
  702.     protected function buildSPILocationCreateStructs(
  703.         array $locationCreateStructs,
  704.         ?ContentType $contentType null
  705.     ): array {
  706.         $spiLocationCreateStructs = [];
  707.         $parentLocationIdSet = [];
  708.         $mainLocation true;
  709.         foreach ($locationCreateStructs as $locationCreateStruct) {
  710.             if (isset($parentLocationIdSet[$locationCreateStruct->parentLocationId])) {
  711.                 throw new InvalidArgumentException(
  712.                     '$locationCreateStructs',
  713.                     "You provided multiple LocationCreateStructs with the same parent Location '{$locationCreateStruct->parentLocationId}'"
  714.                 );
  715.             }
  716.             if ($locationCreateStruct->sortField === null) {
  717.                 $locationCreateStruct->sortField $contentType->defaultSortField ?? Location::SORT_FIELD_NAME;
  718.             }
  719.             if ($locationCreateStruct->sortOrder === null) {
  720.                 $locationCreateStruct->sortOrder $contentType->defaultSortOrder ?? Location::SORT_ORDER_ASC;
  721.             }
  722.             $parentLocationIdSet[$locationCreateStruct->parentLocationId] = true;
  723.             $parentLocation $this->repository->getLocationService()->loadLocation(
  724.                 $locationCreateStruct->parentLocationId
  725.             );
  726.             $spiLocationCreateStructs[] = $this->contentDomainMapper->buildSPILocationCreateStruct(
  727.                 $locationCreateStruct,
  728.                 $parentLocation,
  729.                 $mainLocation,
  730.                 // For Content draft contentId and contentVersionNo are set in ContentHandler upon draft creation
  731.                 null,
  732.                 null,
  733.                 false
  734.             );
  735.             // First Location in the list will be created as main Location
  736.             $mainLocation false;
  737.         }
  738.         return $spiLocationCreateStructs;
  739.     }
  740.     /**
  741.      * Updates the metadata.
  742.      *
  743.      * (see {@link ContentMetadataUpdateStruct}) of a content object - to update fields use updateContent
  744.      *
  745.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update the content meta data
  746.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the remoteId in $contentMetadataUpdateStruct is set but already exists
  747.      *
  748.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  749.      * @param \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct $contentMetadataUpdateStruct
  750.      *
  751.      * @return \eZ\Publish\API\Repository\Values\Content\Content the content with the updated attributes
  752.      */
  753.     public function updateContentMetadata(ContentInfo $contentInfoContentMetadataUpdateStruct $contentMetadataUpdateStruct): APIContent
  754.     {
  755.         $propertyCount 0;
  756.         foreach ($contentMetadataUpdateStruct as $propertyName => $propertyValue) {
  757.             if (isset($contentMetadataUpdateStruct->$propertyName)) {
  758.                 ++$propertyCount;
  759.             }
  760.         }
  761.         if ($propertyCount === 0) {
  762.             throw new InvalidArgumentException(
  763.                 '$contentMetadataUpdateStruct',
  764.                 'At least one property must be set'
  765.             );
  766.         }
  767.         $loadedContentInfo $this->loadContentInfo($contentInfo->id);
  768.         if (!$this->permissionResolver->canUser('content''edit'$loadedContentInfo)) {
  769.             throw new UnauthorizedException('content''edit', ['contentId' => $loadedContentInfo->id]);
  770.         }
  771.         if (isset($contentMetadataUpdateStruct->remoteId)) {
  772.             try {
  773.                 $existingContentInfo $this->loadContentInfoByRemoteId($contentMetadataUpdateStruct->remoteId);
  774.                 if ($existingContentInfo->id !== $loadedContentInfo->id) {
  775.                     throw new InvalidArgumentException(
  776.                         '$contentMetadataUpdateStruct',
  777.                         "Another Content item with remoteId '{$contentMetadataUpdateStruct->remoteId}' exists"
  778.                     );
  779.                 }
  780.             } catch (APINotFoundException $e) {
  781.                 // Do nothing
  782.             }
  783.         }
  784.         $this->repository->beginTransaction();
  785.         try {
  786.             if ($propertyCount || !isset($contentMetadataUpdateStruct->mainLocationId)) {
  787.                 $this->persistenceHandler->contentHandler()->updateMetadata(
  788.                     $loadedContentInfo->id,
  789.                     new SPIMetadataUpdateStruct(
  790.                         [
  791.                             'ownerId' => $contentMetadataUpdateStruct->ownerId,
  792.                             'publicationDate' => isset($contentMetadataUpdateStruct->publishedDate) ?
  793.                                 $contentMetadataUpdateStruct->publishedDate->getTimestamp() :
  794.                                 null,
  795.                             'modificationDate' => isset($contentMetadataUpdateStruct->modificationDate) ?
  796.                                 $contentMetadataUpdateStruct->modificationDate->getTimestamp() :
  797.                                 null,
  798.                             'mainLanguageId' => isset($contentMetadataUpdateStruct->mainLanguageCode) ?
  799.                                 $this->repository->getContentLanguageService()->loadLanguage(
  800.                                     $contentMetadataUpdateStruct->mainLanguageCode
  801.                                 )->id :
  802.                                 null,
  803.                             'alwaysAvailable' => $contentMetadataUpdateStruct->alwaysAvailable,
  804.                             'remoteId' => $contentMetadataUpdateStruct->remoteId,
  805.                             'name' => $contentMetadataUpdateStruct->name,
  806.                         ]
  807.                     )
  808.                 );
  809.             }
  810.             // Change main location
  811.             if (isset($contentMetadataUpdateStruct->mainLocationId)
  812.                 && $loadedContentInfo->mainLocationId !== $contentMetadataUpdateStruct->mainLocationId) {
  813.                 $this->persistenceHandler->locationHandler()->changeMainLocation(
  814.                     $loadedContentInfo->id,
  815.                     $contentMetadataUpdateStruct->mainLocationId
  816.                 );
  817.             }
  818.             // Republish URL aliases to update always-available flag
  819.             if (isset($contentMetadataUpdateStruct->alwaysAvailable)
  820.                 && $loadedContentInfo->alwaysAvailable !== $contentMetadataUpdateStruct->alwaysAvailable) {
  821.                 $content $this->loadContent($loadedContentInfo->id);
  822.                 $this->publishUrlAliasesForContent($contentfalse);
  823.             }
  824.             $this->repository->commit();
  825.         } catch (Exception $e) {
  826.             $this->repository->rollback();
  827.             throw $e;
  828.         }
  829.         return isset($content) ? $content $this->loadContent($loadedContentInfo->id);
  830.     }
  831.     /**
  832.      * Publishes URL aliases for all locations of a given content.
  833.      *
  834.      * @param \eZ\Publish\API\Repository\Values\Content\Content $content
  835.      * @param bool $updatePathIdentificationString this parameter is legacy storage specific for updating
  836.      *                      ezcontentobject_tree.path_identification_string, it is ignored by other storage engines
  837.      */
  838.     protected function publishUrlAliasesForContent(APIContent $contentbool $updatePathIdentificationString true): void
  839.     {
  840.         $urlAliasNames $this->nameSchemaService->resolveUrlAliasSchema($content);
  841.         $locations $this->repository->getLocationService()->loadLocations(
  842.             $content->getVersionInfo()->getContentInfo()
  843.         );
  844.         $urlAliasHandler $this->persistenceHandler->urlAliasHandler();
  845.         foreach ($locations as $location) {
  846.             foreach ($urlAliasNames as $languageCode => $name) {
  847.                 $urlAliasHandler->publishUrlAliasForLocation(
  848.                     $location->id,
  849.                     $location->parentLocationId,
  850.                     $name,
  851.                     $languageCode,
  852.                     $content->contentInfo->alwaysAvailable,
  853.                     $updatePathIdentificationString $languageCode === $content->contentInfo->mainLanguageCode false
  854.                 );
  855.             }
  856.             // archive URL aliases of Translations that got deleted
  857.             $urlAliasHandler->archiveUrlAliasesForDeletedTranslations(
  858.                 $location->id,
  859.                 $location->parentLocationId,
  860.                 $content->versionInfo->languageCodes
  861.             );
  862.         }
  863.     }
  864.     /**
  865.      * Deletes a content object including all its versions and locations including their subtrees.
  866.      *
  867.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to delete the content (in one of the locations of the given content object)
  868.      *
  869.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  870.      *
  871.      * @return mixed[] Affected Location Id's
  872.      */
  873.     public function deleteContent(ContentInfo $contentInfo): iterable
  874.     {
  875.         $contentInfo $this->internalLoadContentInfoById($contentInfo->id);
  876.         $versionInfo $this->persistenceHandler->contentHandler()->loadVersionInfo(
  877.             $contentInfo->id,
  878.             $contentInfo->currentVersionNo
  879.         );
  880.         $translations $versionInfo->languageCodes;
  881.         $target = (new Target\Version())->deleteTranslations($translations);
  882.         if (!$this->permissionResolver->canUser('content''remove'$contentInfo, [$target])) {
  883.             throw new UnauthorizedException('content''remove', ['contentId' => $contentInfo->id]);
  884.         }
  885.         $affectedLocations = [];
  886.         $this->repository->beginTransaction();
  887.         try {
  888.             // Load Locations first as deleting Content also deletes belonging Locations
  889.             $spiLocations $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentInfo->id);
  890.             $this->persistenceHandler->contentHandler()->deleteContent($contentInfo->id);
  891.             $urlAliasHandler $this->persistenceHandler->urlAliasHandler();
  892.             foreach ($spiLocations as $spiLocation) {
  893.                 $urlAliasHandler->locationDeleted($spiLocation->id);
  894.                 $affectedLocations[] = $spiLocation->id;
  895.             }
  896.             $this->repository->commit();
  897.         } catch (Exception $e) {
  898.             $this->repository->rollback();
  899.             throw $e;
  900.         }
  901.         return $affectedLocations;
  902.     }
  903.     /**
  904.      * Creates a draft from a published or archived version.
  905.      *
  906.      * If no version is given, the current published version is used.
  907.      *
  908.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  909.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo|null $versionInfo
  910.      * @param \eZ\Publish\API\Repository\Values\User\User|null $creator if set given user is used to create the draft - otherwise the current-user is used
  911.      * @param \eZ\Publish\API\Repository\Values\Content\Language|null if not set the draft is created with the initialLanguage code of the source version or if not present with the main language.
  912.      *
  913.      * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
  914.      *
  915.      * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
  916.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the current-user is not allowed to create the draft
  917.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to create the draft
  918.      */
  919.     public function createContentDraft(
  920.         ContentInfo $contentInfo,
  921.         ?APIVersionInfo $versionInfo null,
  922.         ?User $creator null,
  923.         ?Language $language null
  924.     ): APIContent {
  925.         $contentInfo $this->loadContentInfo($contentInfo->id);
  926.         if ($versionInfo !== null) {
  927.             // Check that given $contentInfo and $versionInfo belong to the same content
  928.             if ($versionInfo->getContentInfo()->id != $contentInfo->id) {
  929.                 throw new InvalidArgumentException(
  930.                     '$versionInfo',
  931.                     'VersionInfo does not belong to the same Content item as the given ContentInfo'
  932.                 );
  933.             }
  934.             $versionInfo $this->loadVersionInfoById($contentInfo->id$versionInfo->versionNo);
  935.             switch ($versionInfo->status) {
  936.                 case VersionInfo::STATUS_PUBLISHED:
  937.                 case VersionInfo::STATUS_ARCHIVED:
  938.                     break;
  939.                 default:
  940.                     // @todo: throw an exception here, to be defined
  941.                     throw new BadStateException(
  942.                         '$versionInfo',
  943.                         'Cannot create a draft from a draft version'
  944.                     );
  945.             }
  946.             $versionNo $versionInfo->versionNo;
  947.         } elseif ($contentInfo->published) {
  948.             $versionNo $contentInfo->currentVersionNo;
  949.         } else {
  950.             // @todo: throw an exception here, to be defined
  951.             throw new BadStateException(
  952.                 '$contentInfo',
  953.                 'Content is not published. A draft can be created only from a published or archived version.'
  954.             );
  955.         }
  956.         if ($creator === null) {
  957.             $creator $this->permissionResolver->getCurrentUserReference();
  958.         }
  959.         $fallbackLanguageCode $versionInfo->initialLanguageCode ?? $contentInfo->mainLanguageCode;
  960.         $languageCode $language->languageCode ?? $fallbackLanguageCode;
  961.         if (!$this->permissionResolver->canUser(
  962.             'content',
  963.             'edit',
  964.             $contentInfo,
  965.             [
  966.                 (new Target\Builder\VersionBuilder())
  967.                     ->changeStatusTo(APIVersionInfo::STATUS_DRAFT)
  968.                     ->build(),
  969.             ]
  970.         )) {
  971.             throw new UnauthorizedException(
  972.                 'content',
  973.                 'edit',
  974.                 ['contentId' => $contentInfo->id]
  975.             );
  976.         }
  977.         $this->repository->beginTransaction();
  978.         try {
  979.             $spiContent $this->persistenceHandler->contentHandler()->createDraftFromVersion(
  980.                 $contentInfo->id,
  981.                 $versionNo,
  982.                 $creator->getUserId(),
  983.                 $languageCode
  984.             );
  985.             $this->repository->commit();
  986.         } catch (Exception $e) {
  987.             $this->repository->rollback();
  988.             throw $e;
  989.         }
  990.         return $this->contentDomainMapper->buildContentDomainObject(
  991.             $spiContent,
  992.             $this->repository->getContentTypeService()->loadContentType(
  993.                 $spiContent->versionInfo->contentInfo->contentTypeId
  994.             )
  995.         );
  996.     }
  997.     public function countContentDrafts(?User $user null): int
  998.     {
  999.         if ($this->permissionResolver->hasAccess('content''versionread') === false) {
  1000.             return 0;
  1001.         }
  1002.         return $this->persistenceHandler->contentHandler()->countDraftsForUser(
  1003.             $this->resolveUser($user)->getUserId()
  1004.         );
  1005.     }
  1006.     /**
  1007.      * Loads drafts for a user.
  1008.      *
  1009.      * If no user is given the drafts for the authenticated user are returned
  1010.      *
  1011.      * @param \eZ\Publish\API\Repository\Values\User\User|null $user
  1012.      *
  1013.      * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Drafts owned by the given user
  1014.      *
  1015.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
  1016.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
  1017.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  1018.      */
  1019.     public function loadContentDrafts(?User $user null): iterable
  1020.     {
  1021.         // throw early if user has absolutely no access to versionread
  1022.         if ($this->permissionResolver->hasAccess('content''versionread') === false) {
  1023.             throw new UnauthorizedException('content''versionread');
  1024.         }
  1025.         $spiVersionInfoList $this->persistenceHandler->contentHandler()->loadDraftsForUser(
  1026.             $this->resolveUser($user)->getUserId()
  1027.         );
  1028.         $versionInfoList = [];
  1029.         foreach ($spiVersionInfoList as $spiVersionInfo) {
  1030.             $versionInfo $this->contentDomainMapper->buildVersionInfoDomainObject($spiVersionInfo);
  1031.             // @todo: Change this to filter returned drafts by permissions instead of throwing
  1032.             if (!$this->permissionResolver->canUser('content''versionread'$versionInfo)) {
  1033.                 throw new UnauthorizedException('content''versionread', ['contentId' => $versionInfo->contentInfo->id]);
  1034.             }
  1035.             $versionInfoList[] = $versionInfo;
  1036.         }
  1037.         return $versionInfoList;
  1038.     }
  1039.     public function loadContentDraftList(?User $user nullint $offset 0int $limit = -1): ContentDraftList
  1040.     {
  1041.         $list = new ContentDraftList();
  1042.         if ($this->permissionResolver->hasAccess('content''versionread') === false) {
  1043.             return $list;
  1044.         }
  1045.         $list->totalCount $this->persistenceHandler->contentHandler()->countDraftsForUser(
  1046.             $this->resolveUser($user)->getUserId()
  1047.         );
  1048.         if ($list->totalCount 0) {
  1049.             $spiVersionInfoList $this->persistenceHandler->contentHandler()->loadDraftListForUser(
  1050.                 $this->resolveUser($user)->getUserId(),
  1051.                 $offset,
  1052.                 $limit
  1053.             );
  1054.             foreach ($spiVersionInfoList as $spiVersionInfo) {
  1055.                 $versionInfo $this->contentDomainMapper->buildVersionInfoDomainObject($spiVersionInfo);
  1056.                 if ($this->permissionResolver->canUser('content''versionread'$versionInfo)) {
  1057.                     $list->items[] = new ContentDraftListItem($versionInfo);
  1058.                 } else {
  1059.                     $list->items[] = new UnauthorizedContentDraftListItem(
  1060.                         'content',
  1061.                         'versionread',
  1062.                         ['contentId' => $versionInfo->contentInfo->id]
  1063.                     );
  1064.                 }
  1065.             }
  1066.         }
  1067.         return $list;
  1068.     }
  1069.     /**
  1070.      * Updates the fields of a draft.
  1071.      *
  1072.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
  1073.      * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
  1074.      *
  1075.      * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the updated fields
  1076.      *
  1077.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
  1078.      *                                                                               or if a required field is missing / set to an empty value.
  1079.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
  1080.      *                                                                          or value is set for non-translatable field in language
  1081.      *                                                                          other than main.
  1082.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
  1083.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
  1084.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
  1085.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
  1086.      */
  1087.     public function updateContent(APIVersionInfo $versionInfoAPIContentUpdateStruct $contentUpdateStruct, ?array $fieldIdentifiersToValidate null): APIContent
  1088.     {
  1089.         /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
  1090.         $content $this->loadContent(
  1091.             $versionInfo->getContentInfo()->id,
  1092.             null,
  1093.             $versionInfo->versionNo
  1094.         );
  1095.         $updatedFields $this->contentMapper->getFieldsForUpdate($contentUpdateStruct->fields$content);
  1096.         if (!$this->repository->getPermissionResolver()->canUser(
  1097.             'content',
  1098.             'edit',
  1099.             $content,
  1100.             [
  1101.                 (new Target\Builder\VersionBuilder())
  1102.                     ->updateFields($updatedFields)
  1103.                     ->updateFieldsTo(
  1104.                         $contentUpdateStruct->initialLanguageCode,
  1105.                         $contentUpdateStruct->fields
  1106.                     )
  1107.                     ->build(),
  1108.             ]
  1109.         )) {
  1110.             throw new UnauthorizedException('content''edit', ['contentId' => $content->id]);
  1111.         }
  1112.         return $this->internalUpdateContent($versionInfo$contentUpdateStruct$fieldIdentifiersToValidate);
  1113.     }
  1114.     /**
  1115.      * Updates the fields of a draft without checking the permissions.
  1116.      *
  1117.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
  1118.      *                                                                               or if a required field is missing / set to an empty value.
  1119.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
  1120.      *                                                                          or value is set for non-translatable field in language
  1121.      *                                                                          other than main.
  1122.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
  1123.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
  1124.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
  1125.      */
  1126.     protected function internalUpdateContent(
  1127.         APIVersionInfo $versionInfo,
  1128.         APIContentUpdateStruct $contentUpdateStruct,
  1129.         ?array $fieldIdentifiersToValidate null
  1130.     ): Content {
  1131.         $contentUpdateStruct = clone $contentUpdateStruct;
  1132.         /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
  1133.         $content $this->internalLoadContentById(
  1134.             $versionInfo->getContentInfo()->id,
  1135.             null,
  1136.             $versionInfo->versionNo
  1137.         );
  1138.         if (!$content->versionInfo->isDraft()) {
  1139.             throw new BadStateException(
  1140.                 '$versionInfo',
  1141.                 'The version is not a draft and cannot be updated'
  1142.             );
  1143.         }
  1144.         $errors $this->validate(
  1145.             $contentUpdateStruct,
  1146.             ['content' => $content],
  1147.             $fieldIdentifiersToValidate
  1148.         );
  1149.         if (!empty($errors)) {
  1150.             throw new ContentFieldValidationException($errors);
  1151.         }
  1152.         $mainLanguageCode $content->contentInfo->mainLanguageCode;
  1153.         if ($contentUpdateStruct->initialLanguageCode === null) {
  1154.             $contentUpdateStruct->initialLanguageCode $mainLanguageCode;
  1155.         }
  1156.         $allLanguageCodes $this->contentMapper->getLanguageCodesForUpdate($contentUpdateStruct$content);
  1157.         $contentLanguageHandler $this->persistenceHandler->contentLanguageHandler();
  1158.         foreach ($allLanguageCodes as $languageCode) {
  1159.             $contentLanguageHandler->loadByLanguageCode($languageCode);
  1160.         }
  1161.         $contentType $this->repository->getContentTypeService()->loadContentType(
  1162.             $content->contentInfo->contentTypeId
  1163.         );
  1164.         $fields $this->contentMapper->mapFieldsForUpdate(
  1165.             $contentUpdateStruct,
  1166.             $contentType,
  1167.             $mainLanguageCode
  1168.         );
  1169.         $fieldValues = [];
  1170.         $spiFields = [];
  1171.         $inputRelations = [];
  1172.         $locationIdToContentIdMapping = [];
  1173.         foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
  1174.             $fieldType $this->fieldTypeRegistry->getFieldType(
  1175.                 $fieldDefinition->fieldTypeIdentifier
  1176.             );
  1177.             foreach ($allLanguageCodes as $languageCode) {
  1178.                 $isCopied $isEmpty $isRetained false;
  1179.                 $isLanguageNew = !in_array($languageCode$content->versionInfo->languageCodes);
  1180.                 $valueLanguageCode $fieldDefinition->isTranslatable $languageCode $mainLanguageCode;
  1181.                 $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
  1182.                 $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
  1183.                 if (!$isFieldUpdated && !$isLanguageNew) {
  1184.                     $isRetained true;
  1185.                 } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
  1186.                     $isCopied true;
  1187.                 }
  1188.                 $fieldValue $this->contentMapper->getFieldValueForUpdate(
  1189.                     $fields[$fieldDefinition->identifier][$valueLanguageCode] ?? null,
  1190.                     $content->getField($fieldDefinition->identifier$valueLanguageCode),
  1191.                     $fieldDefinition,
  1192.                     $isLanguageNew
  1193.                 );
  1194.                 if ($fieldType->isEmptyValue($fieldValue)) {
  1195.                     $isEmpty true;
  1196.                 }
  1197.                 $this->relationProcessor->appendFieldRelations(
  1198.                     $inputRelations,
  1199.                     $locationIdToContentIdMapping,
  1200.                     $fieldType,
  1201.                     $fieldValue,
  1202.                     $fieldDefinition->id
  1203.                 );
  1204.                 $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
  1205.                 if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
  1206.                     continue;
  1207.                 }
  1208.                 $spiFields[] = new SPIField(
  1209.                     [
  1210.                         'id' => $isLanguageNew ?
  1211.                             null :
  1212.                             $content->getField($fieldDefinition->identifier$languageCode)->id,
  1213.                         'fieldDefinitionId' => $fieldDefinition->id,
  1214.                         'type' => $fieldDefinition->fieldTypeIdentifier,
  1215.                         'value' => $fieldType->toPersistenceValue($fieldValue),
  1216.                         'languageCode' => $languageCode,
  1217.                         'versionNo' => $versionInfo->versionNo,
  1218.                     ]
  1219.                 );
  1220.             }
  1221.         }
  1222.         $spiContentUpdateStruct = new SPIContentUpdateStruct(
  1223.             [
  1224.                 'name' => $this->nameSchemaService->resolveNameSchema(
  1225.                     $content,
  1226.                     $fieldValues,
  1227.                     $allLanguageCodes,
  1228.                     $contentType
  1229.                 ),
  1230.                 'creatorId' => $contentUpdateStruct->creatorId ?: $this->permissionResolver->getCurrentUserReference()->getUserId(),
  1231.                 'fields' => $spiFields,
  1232.                 'modificationDate' => time(),
  1233.                 'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
  1234.                     $contentUpdateStruct->initialLanguageCode
  1235.                 )->id,
  1236.             ]
  1237.         );
  1238.         $existingRelations $this->internalLoadRelations($versionInfo);
  1239.         $this->repository->beginTransaction();
  1240.         try {
  1241.             $spiContent $this->persistenceHandler->contentHandler()->updateContent(
  1242.                 $versionInfo->getContentInfo()->id,
  1243.                 $versionInfo->versionNo,
  1244.                 $spiContentUpdateStruct
  1245.             );
  1246.             $this->relationProcessor->processFieldRelations(
  1247.                 $inputRelations,
  1248.                 $spiContent->versionInfo->contentInfo->id,
  1249.                 $spiContent->versionInfo->versionNo,
  1250.                 $contentType,
  1251.                 $existingRelations
  1252.             );
  1253.             $this->repository->commit();
  1254.         } catch (Exception $e) {
  1255.             $this->repository->rollback();
  1256.             throw $e;
  1257.         }
  1258.         return $this->contentDomainMapper->buildContentDomainObject(
  1259.             $spiContent,
  1260.             $contentType
  1261.         );
  1262.     }
  1263.     /**
  1264.      * Publishes a content version.
  1265.      *
  1266.      * Publishes a content version and deletes archive versions if they overflow max archive versions.
  1267.      * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
  1268.      *
  1269.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
  1270.      * @param string[] $translations
  1271.      *
  1272.      * @return \eZ\Publish\API\Repository\Values\Content\Content
  1273.      *
  1274.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
  1275.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  1276.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
  1277.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
  1278.      */
  1279.     public function publishVersion(APIVersionInfo $versionInfo, array $translations Language::ALL): APIContent
  1280.     {
  1281.         $content $this->internalLoadContentById(
  1282.             $versionInfo->contentInfo->id,
  1283.             null,
  1284.             $versionInfo->versionNo
  1285.         );
  1286.         $targets = [];
  1287.         if (!empty($translations)) {
  1288.             $targets[] = (new Target\Builder\VersionBuilder())
  1289.                 ->publishTranslations($translations)
  1290.                 ->build();
  1291.         }
  1292.         if (!$this->permissionResolver->canUser(
  1293.             'content',
  1294.             'publish',
  1295.             $content,
  1296.             $targets
  1297.         )) {
  1298.             throw new UnauthorizedException(
  1299.                 'content''publish', ['contentId' => $content->id]
  1300.             );
  1301.         }
  1302.         $this->repository->beginTransaction();
  1303.         try {
  1304.             $this->copyTranslationsFromPublishedVersion($content->versionInfo$translations);
  1305.             $content $this->internalPublishVersion($content->getVersionInfo(), null);
  1306.             $this->repository->commit();
  1307.         } catch (Exception $e) {
  1308.             $this->repository->rollback();
  1309.             throw $e;
  1310.         }
  1311.         return $content;
  1312.     }
  1313.     /**
  1314.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
  1315.      * @param array $translations
  1316.      *
  1317.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
  1318.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException
  1319.      * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
  1320.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
  1321.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
  1322.      */
  1323.     protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionInfo, array $translations = []): void
  1324.     {
  1325.         $contendId $versionInfo->contentInfo->id;
  1326.         $currentContent $this->internalLoadContentById($contendId);
  1327.         $currentVersionInfo $currentContent->versionInfo;
  1328.         // Copying occurs only if:
  1329.         // - There is published Version
  1330.         // - Published version is older than the currently published one unless specific translations are provided.
  1331.         if (!$currentVersionInfo->isPublished() ||
  1332.             ($versionInfo->versionNo >= $currentVersionInfo->versionNo && empty($translations))) {
  1333.             return;
  1334.         }
  1335.         if (empty($translations)) {
  1336.             $languagesToCopy array_diff(
  1337.                 $currentVersionInfo->languageCodes,
  1338.                 $versionInfo->languageCodes
  1339.             );
  1340.         } else {
  1341.             $languagesToCopy array_diff(
  1342.                 $currentVersionInfo->languageCodes,
  1343.                 $translations
  1344.             );
  1345.         }
  1346.         if (empty($languagesToCopy)) {
  1347.             return;
  1348.         }
  1349.         $contentType $this->repository->getContentTypeService()->loadContentType(
  1350.             $currentVersionInfo->contentInfo->contentTypeId
  1351.         );
  1352.         // Find only translatable fields to update with selected languages
  1353.         $updateStruct $this->newContentUpdateStruct();
  1354.         $updateStruct->initialLanguageCode $versionInfo->initialLanguageCode;
  1355.         $contentToPublish $this->internalLoadContentById($contendIdnull$versionInfo->versionNo);
  1356.         $fallbackUpdateStruct $this->newContentUpdateStruct();
  1357.         foreach ($currentContent->getFields() as $field) {
  1358.             $fieldDefinition $contentType->getFieldDefinition($field->fieldDefIdentifier);
  1359.             if (!$fieldDefinition->isTranslatable || !\in_array($field->languageCode$languagesToCopy)) {
  1360.                 continue;
  1361.             }
  1362.             $fieldType $this->fieldTypeRegistry->getFieldType(
  1363.                 $fieldDefinition->fieldTypeIdentifier
  1364.             );
  1365.             $newValue $contentToPublish->getFieldValue(
  1366.                 $fieldDefinition->identifier,
  1367.                 $field->languageCode
  1368.             );
  1369.             $value $field->value;
  1370.             if ($fieldDefinition->isRequired && $fieldType->isEmptyValue($value)) {
  1371.                 if (!$fieldType->isEmptyValue($fieldDefinition->defaultValue)) {
  1372.                     $value $fieldDefinition->defaultValue;
  1373.                 } else {
  1374.                     $value $contentToPublish->getFieldValue($field->fieldDefIdentifier$versionInfo->initialLanguageCode);
  1375.                 }
  1376.                 $fallbackUpdateStruct->setField(
  1377.                     $field->fieldDefIdentifier,
  1378.                     $value,
  1379.                     $field->languageCode
  1380.                 );
  1381.                 continue;
  1382.             }
  1383.             if ($newValue !== null
  1384.                 && $field->value !== null
  1385.                 && $this->fieldValuesAreEqual($fieldType$newValue$field->value)
  1386.             ) {
  1387.                 continue;
  1388.             }
  1389.             $updateStruct->setField($field->fieldDefIdentifier$value$field->languageCode);
  1390.         }
  1391.         // Nothing to copy, skip update
  1392.         if (empty($updateStruct->fields)) {
  1393.             return;
  1394.         }
  1395.         // Do fallback only if content needs to be updated
  1396.         foreach ($fallbackUpdateStruct->fields as $fallbackField) {
  1397.             $updateStruct->setField($fallbackField->fieldDefIdentifier$fallbackField->value$fallbackField->languageCode);
  1398.         }
  1399.         $this->internalUpdateContent($versionInfo$updateStruct);
  1400.     }
  1401.     protected function fieldValuesAreEqual(FieldType $fieldTypeValue $value1Value $value2): bool
  1402.     {
  1403.         if ($fieldType instanceof Comparable) {
  1404.             return $fieldType->valuesEqual($value1$value2);
  1405.         } else {
  1406.             @trigger_error(
  1407.                 \sprintf(
  1408.                     'In eZ Platform 2.5 and 3.x %s should implement %s. ' .
  1409.                     'Since the 4.0 major release FieldType\Comparable contract will be a part of %s',
  1410.                     get_class($fieldType),
  1411.                     Comparable::class,
  1412.                     FieldType::class
  1413.                 ),
  1414.                 E_USER_DEPRECATED
  1415.             );
  1416.             return $fieldType->toHash($value1) === $fieldType->toHash($value2);
  1417.         }
  1418.     }
  1419.     /**
  1420.      * Publishes a content version.
  1421.      *
  1422.      * Publishes a content version and deletes archive versions if they overflow max archive versions.
  1423.      * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
  1424.      *
  1425.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
  1426.      *
  1427.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
  1428.      * @param int|null $publicationDate If null existing date is kept if there is one, otherwise current time is used.
  1429.      *
  1430.      * @return \eZ\Publish\API\Repository\Values\Content\Content
  1431.      */
  1432.     protected function internalPublishVersion(APIVersionInfo $versionInfo$publicationDate null)
  1433.     {
  1434.         if (!$versionInfo->isDraft()) {
  1435.             throw new BadStateException('$versionInfo''Only versions in draft status can be published.');
  1436.         }
  1437.         $currentTime $this->getUnixTimestamp();
  1438.         if ($publicationDate === null && $versionInfo->versionNo === 1) {
  1439.             $publicationDate $currentTime;
  1440.         }
  1441.         $errors $this->validate(
  1442.             $versionInfo, [
  1443.                 'content' => $this->internalLoadContentById(
  1444.                     $versionInfo->getContentInfo()->id,
  1445.                     null,
  1446.                     $versionInfo->versionNo
  1447.                 ),
  1448.             ]
  1449.         );
  1450.         if (!empty($errors)) {
  1451.             throw new ContentFieldValidationException($errors);
  1452.         }
  1453.         $contentInfo $versionInfo->getContentInfo();
  1454.         $metadataUpdateStruct = new SPIMetadataUpdateStruct();
  1455.         $metadataUpdateStruct->publicationDate $publicationDate;
  1456.         $metadataUpdateStruct->modificationDate $currentTime;
  1457.         $metadataUpdateStruct->isHidden $contentInfo->isHidden;
  1458.         $contentId $contentInfo->id;
  1459.         $spiContent $this->persistenceHandler->contentHandler()->publish(
  1460.             $contentId,
  1461.             $versionInfo->versionNo,
  1462.             $metadataUpdateStruct
  1463.         );
  1464.         $content $this->contentDomainMapper->buildContentDomainObject(
  1465.             $spiContent,
  1466.             $this->repository->getContentTypeService()->loadContentType(
  1467.                 $spiContent->versionInfo->contentInfo->contentTypeId
  1468.             )
  1469.         );
  1470.         $this->publishUrlAliasesForContent($content);
  1471.         if ($this->settings['remove_archived_versions_on_publish']) {
  1472.             $this->deleteArchivedVersionsOverLimit($contentId);
  1473.         }
  1474.         return $content;
  1475.     }
  1476.     protected function deleteArchivedVersionsOverLimit(int $contentId): void
  1477.     {
  1478.         // Delete version archive overflow if any, limit is 0-50 (however 0 will mean 1 if content is unpublished)
  1479.         $archiveList $this->persistenceHandler->contentHandler()->listVersions(
  1480.             $contentId,
  1481.             APIVersionInfo::STATUS_ARCHIVED,
  1482.             100 // Limited to avoid publishing taking to long, besides SE limitations this is why limit is max 50
  1483.         );
  1484.         $maxVersionArchiveCount max(0min(50$this->settings['default_version_archive_limit']));
  1485.         while (!empty($archiveList) && count($archiveList) > $maxVersionArchiveCount) {
  1486.             /** @var \eZ\Publish\SPI\Persistence\Content\VersionInfo $archiveVersion */
  1487.             $archiveVersion array_shift($archiveList);
  1488.             $this->persistenceHandler->contentHandler()->deleteVersion(
  1489.                 $contentId,
  1490.                 $archiveVersion->versionNo
  1491.             );
  1492.         }
  1493.     }
  1494.     /**
  1495.      * @return int
  1496.      */
  1497.     protected function getUnixTimestamp(): int
  1498.     {
  1499.         return time();
  1500.     }
  1501.     /**
  1502.      * Removes the given version.
  1503.      *
  1504.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is in
  1505.      *         published state or is a last version of Content in non draft state
  1506.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to remove this version
  1507.      *
  1508.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
  1509.      */
  1510.     public function deleteVersion(APIVersionInfo $versionInfo): void
  1511.     {
  1512.         $contentHandler $this->persistenceHandler->contentHandler();
  1513.         if ($versionInfo->isPublished()) {
  1514.             throw new BadStateException(
  1515.                 '$versionInfo',
  1516.                 'The Version is published and cannot be removed'
  1517.             );
  1518.         }
  1519.         if (!$this->permissionResolver->canUser('content''versionremove'$versionInfo)) {
  1520.             throw new UnauthorizedException(
  1521.                 'content',
  1522.                 'versionremove',
  1523.                 ['contentId' => $versionInfo->contentInfo->id'versionNo' => $versionInfo->versionNo]
  1524.             );
  1525.         }
  1526.         $versionList $contentHandler->listVersions(
  1527.             $versionInfo->contentInfo->id,
  1528.             null,
  1529.             2
  1530.         );
  1531.         $versionsCount count($versionList);
  1532.         if ($versionsCount === && !$versionInfo->isDraft()) {
  1533.             throw new BadStateException(
  1534.                 '$versionInfo',
  1535.                 'The Version is the last version of the Content item and cannot be removed'
  1536.             );
  1537.         }
  1538.         $this->repository->beginTransaction();
  1539.         try {
  1540.             if ($versionsCount === 1) {
  1541.                 $contentHandler->deleteContent($versionInfo->contentInfo->id);
  1542.             } else {
  1543.                 $contentHandler->deleteVersion(
  1544.                     $versionInfo->getContentInfo()->id,
  1545.                     $versionInfo->versionNo
  1546.                 );
  1547.             }
  1548.             $this->repository->commit();
  1549.         } catch (Exception $e) {
  1550.             $this->repository->rollback();
  1551.             throw $e;
  1552.         }
  1553.     }
  1554.     /**
  1555.      * Loads all versions for the given content.
  1556.      *
  1557.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to list versions
  1558.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the given status is invalid
  1559.      *
  1560.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  1561.      * @param int|null $status
  1562.      *
  1563.      * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Sorted by creation date
  1564.      */
  1565.     public function loadVersions(ContentInfo $contentInfo, ?int $status null): iterable
  1566.     {
  1567.         if (!$this->permissionResolver->canUser('content''versionread'$contentInfo)) {
  1568.             throw new UnauthorizedException('content''versionread', ['contentId' => $contentInfo->id]);
  1569.         }
  1570.         if ($status !== null && !in_array((int)$status, [VersionInfo::STATUS_DRAFTVersionInfo::STATUS_PUBLISHEDVersionInfo::STATUS_ARCHIVED], true)) {
  1571.             throw new InvalidArgumentException(
  1572.                 'status',
  1573.                 sprintf(
  1574.                     'available statuses are: %d (draft), %d (published), %d (archived), %d given',
  1575.                     VersionInfo::STATUS_DRAFTVersionInfo::STATUS_PUBLISHEDVersionInfo::STATUS_ARCHIVED$status
  1576.                 ));
  1577.         }
  1578.         $spiVersionInfoList $this->persistenceHandler->contentHandler()->listVersions($contentInfo->id$status);
  1579.         $versions = [];
  1580.         foreach ($spiVersionInfoList as $spiVersionInfo) {
  1581.             $versionInfo $this->contentDomainMapper->buildVersionInfoDomainObject($spiVersionInfo);
  1582.             if (!$this->permissionResolver->canUser('content''versionread'$versionInfo)) {
  1583.                 throw new UnauthorizedException('content''versionread', ['versionId' => $versionInfo->id]);
  1584.             }
  1585.             $versions[] = $versionInfo;
  1586.         }
  1587.         return $versions;
  1588.     }
  1589.     /**
  1590.      * Copies the content to a new location. If no version is given,
  1591.      * all versions are copied, otherwise only the given version.
  1592.      *
  1593.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to copy the content to the given location
  1594.      *
  1595.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  1596.      * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $destinationLocationCreateStruct the target location where the content is copied to
  1597.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
  1598.      *
  1599.      * @return \eZ\Publish\API\Repository\Values\Content\Content
  1600.      */
  1601.     public function copyContent(ContentInfo $contentInfoLocationCreateStruct $destinationLocationCreateStruct, ?APIVersionInfo $versionInfo null): APIContent
  1602.     {
  1603.         $destinationLocation $this->repository->getLocationService()->loadLocation(
  1604.             $destinationLocationCreateStruct->parentLocationId
  1605.         );
  1606.         if (!$this->permissionResolver->canUser('content''create'$contentInfo, [$destinationLocation])) {
  1607.             throw new UnauthorizedException(
  1608.                 'content',
  1609.                 'create',
  1610.                 [
  1611.                     'parentLocationId' => $destinationLocationCreateStruct->parentLocationId,
  1612.                     'sectionId' => $contentInfo->sectionId,
  1613.                 ]
  1614.             );
  1615.         }
  1616.         if (!$this->permissionResolver->canUser('content''manage_locations'$contentInfo, [$destinationLocation])) {
  1617.             throw new UnauthorizedException('content''manage_locations', ['contentId' => $contentInfo->id]);
  1618.         }
  1619.         $defaultObjectStates $this->getDefaultObjectStates();
  1620.         $this->repository->beginTransaction();
  1621.         try {
  1622.             $spiContent $this->persistenceHandler->contentHandler()->copy(
  1623.                 $contentInfo->id,
  1624.                 $versionInfo $versionInfo->versionNo null,
  1625.                 $this->permissionResolver->getCurrentUserReference()->getUserId()
  1626.             );
  1627.             $objectStateHandler $this->persistenceHandler->objectStateHandler();
  1628.             foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
  1629.                 $objectStateHandler->setContentState(
  1630.                     $spiContent->versionInfo->contentInfo->id,
  1631.                     $objectStateGroupId,
  1632.                     $objectState->id
  1633.                 );
  1634.             }
  1635.             $content $this->internalPublishVersion(
  1636.                 $this->contentDomainMapper->buildVersionInfoDomainObject($spiContent->versionInfo),
  1637.                 $spiContent->versionInfo->creationDate
  1638.             );
  1639.             $this->repository->getLocationService()->createLocation(
  1640.                 $content->getVersionInfo()->getContentInfo(),
  1641.                 $destinationLocationCreateStruct
  1642.             );
  1643.             $this->repository->commit();
  1644.         } catch (Exception $e) {
  1645.             $this->repository->rollback();
  1646.             throw $e;
  1647.         }
  1648.         return $this->internalLoadContentById($content->id);
  1649.     }
  1650.     /**
  1651.      * Loads all outgoing relations for the given version.
  1652.      *
  1653.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
  1654.      *
  1655.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
  1656.      *
  1657.      * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
  1658.      */
  1659.     public function loadRelations(APIVersionInfo $versionInfo): iterable
  1660.     {
  1661.         if ($versionInfo->isPublished()) {
  1662.             $function 'read';
  1663.         } else {
  1664.             $function 'versionread';
  1665.         }
  1666.         if (!$this->permissionResolver->canUser('content'$function$versionInfo)) {
  1667.             throw new UnauthorizedException('content'$function);
  1668.         }
  1669.         return $this->internalLoadRelations($versionInfo);
  1670.     }
  1671.     /**
  1672.      * Loads all outgoing relations for the given version without checking the permissions.
  1673.      *
  1674.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
  1675.      *
  1676.      * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
  1677.      */
  1678.     protected function internalLoadRelations(APIVersionInfo $versionInfo): array
  1679.     {
  1680.         $contentInfo $versionInfo->getContentInfo();
  1681.         $spiRelations $this->persistenceHandler->contentHandler()->loadRelations(
  1682.             $contentInfo->id,
  1683.             $versionInfo->versionNo
  1684.         );
  1685.         /** @var $relations \eZ\Publish\API\Repository\Values\Content\Relation[] */
  1686.         $relations = [];
  1687.         foreach ($spiRelations as $spiRelation) {
  1688.             $destinationContentInfo $this->internalLoadContentInfoById($spiRelation->destinationContentId);
  1689.             if (!$this->permissionResolver->canUser('content''read'$destinationContentInfo)) {
  1690.                 continue;
  1691.             }
  1692.             $relations[] = $this->contentDomainMapper->buildRelationDomainObject(
  1693.                 $spiRelation,
  1694.                 $contentInfo,
  1695.                 $destinationContentInfo
  1696.             );
  1697.         }
  1698.         return $relations;
  1699.     }
  1700.     /**
  1701.      * {@inheritdoc}
  1702.      */
  1703.     public function countReverseRelations(ContentInfo $contentInfo): int
  1704.     {
  1705.         if (!$this->permissionResolver->canUser('content''reverserelatedlist'$contentInfo)) {
  1706.             return 0;
  1707.         }
  1708.         return $this->persistenceHandler->contentHandler()->countReverseRelations(
  1709.             $contentInfo->id
  1710.         );
  1711.     }
  1712.     /**
  1713.      * Loads all incoming relations for a content object.
  1714.      *
  1715.      * The relations come only from published versions of the source content objects
  1716.      *
  1717.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
  1718.      *
  1719.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  1720.      *
  1721.      * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
  1722.      */
  1723.     public function loadReverseRelations(ContentInfo $contentInfo): iterable
  1724.     {
  1725.         if (!$this->permissionResolver->canUser('content''reverserelatedlist'$contentInfo)) {
  1726.             throw new UnauthorizedException('content''reverserelatedlist', ['contentId' => $contentInfo->id]);
  1727.         }
  1728.         $spiRelations $this->persistenceHandler->contentHandler()->loadReverseRelations(
  1729.             $contentInfo->id
  1730.         );
  1731.         $returnArray = [];
  1732.         foreach ($spiRelations as $spiRelation) {
  1733.             $sourceContentInfo $this->internalLoadContentInfoById($spiRelation->sourceContentId);
  1734.             if (!$this->permissionResolver->canUser('content''read'$sourceContentInfo)) {
  1735.                 continue;
  1736.             }
  1737.             $returnArray[] = $this->contentDomainMapper->buildRelationDomainObject(
  1738.                 $spiRelation,
  1739.                 $sourceContentInfo,
  1740.                 $contentInfo
  1741.             );
  1742.         }
  1743.         return $returnArray;
  1744.     }
  1745.     /**
  1746.      * {@inheritdoc}
  1747.      */
  1748.     public function loadReverseRelationList(ContentInfo $contentInfoint $offset 0int $limit = -1): RelationList
  1749.     {
  1750.         $list = new RelationList();
  1751.         if (!$this->repository->getPermissionResolver()->canUser('content''reverserelatedlist'$contentInfo)) {
  1752.             return $list;
  1753.         }
  1754.         $list->totalCount $this->persistenceHandler->contentHandler()->countReverseRelations(
  1755.             $contentInfo->id
  1756.         );
  1757.         if ($list->totalCount 0) {
  1758.             $spiRelationList $this->persistenceHandler->contentHandler()->loadReverseRelationList(
  1759.                 $contentInfo->id,
  1760.                 $offset,
  1761.                 $limit
  1762.             );
  1763.             foreach ($spiRelationList as $spiRelation) {
  1764.                 $sourceContentInfo $this->internalLoadContentInfoById($spiRelation->sourceContentId);
  1765.                 if ($this->repository->getPermissionResolver()->canUser('content''read'$sourceContentInfo)) {
  1766.                     $relation $this->contentDomainMapper->buildRelationDomainObject(
  1767.                         $spiRelation,
  1768.                         $sourceContentInfo,
  1769.                         $contentInfo
  1770.                     );
  1771.                     $list->items[] = new RelationListItem($relation);
  1772.                 } else {
  1773.                     $list->items[] = new UnauthorizedRelationListItem(
  1774.                         'content',
  1775.                         'read',
  1776.                         ['contentId' => $sourceContentInfo->id]
  1777.                     );
  1778.                 }
  1779.             }
  1780.         }
  1781.         return $list;
  1782.     }
  1783.     /**
  1784.      * Adds a relation of type common.
  1785.      *
  1786.      * The source of the relation is the content and version
  1787.      * referenced by $versionInfo.
  1788.      *
  1789.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to edit this version
  1790.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
  1791.      *
  1792.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
  1793.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent the destination of the relation
  1794.      *
  1795.      * @return \eZ\Publish\API\Repository\Values\Content\Relation the newly created relation
  1796.      */
  1797.     public function addRelation(APIVersionInfo $sourceVersionContentInfo $destinationContent): APIRelation
  1798.     {
  1799.         $sourceVersion $this->loadVersionInfoById(
  1800.             $sourceVersion->contentInfo->id,
  1801.             $sourceVersion->versionNo
  1802.         );
  1803.         if (!$sourceVersion->isDraft()) {
  1804.             throw new BadStateException(
  1805.                 '$sourceVersion',
  1806.                 'Relations of type common can only be added to draft versions'
  1807.             );
  1808.         }
  1809.         if (!$this->permissionResolver->canUser('content''edit'$sourceVersion)) {
  1810.             throw new UnauthorizedException('content''edit', ['contentId' => $sourceVersion->contentInfo->id]);
  1811.         }
  1812.         $sourceContentInfo $sourceVersion->getContentInfo();
  1813.         $this->repository->beginTransaction();
  1814.         try {
  1815.             $spiRelation $this->persistenceHandler->contentHandler()->addRelation(
  1816.                 new SPIRelationCreateStruct(
  1817.                     [
  1818.                         'sourceContentId' => $sourceContentInfo->id,
  1819.                         'sourceContentVersionNo' => $sourceVersion->versionNo,
  1820.                         'sourceFieldDefinitionId' => null,
  1821.                         'destinationContentId' => $destinationContent->id,
  1822.                         'type' => APIRelation::COMMON,
  1823.                     ]
  1824.                 )
  1825.             );
  1826.             $this->repository->commit();
  1827.         } catch (Exception $e) {
  1828.             $this->repository->rollback();
  1829.             throw $e;
  1830.         }
  1831.         return $this->contentDomainMapper->buildRelationDomainObject($spiRelation$sourceContentInfo$destinationContent);
  1832.     }
  1833.     /**
  1834.      * Removes a relation of type COMMON from a draft.
  1835.      *
  1836.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed edit this version
  1837.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
  1838.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is no relation of type COMMON for the given destination
  1839.      *
  1840.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
  1841.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent
  1842.      */
  1843.     public function deleteRelation(APIVersionInfo $sourceVersionContentInfo $destinationContent): void
  1844.     {
  1845.         $sourceVersion $this->loadVersionInfoById(
  1846.             $sourceVersion->contentInfo->id,
  1847.             $sourceVersion->versionNo
  1848.         );
  1849.         if (!$sourceVersion->isDraft()) {
  1850.             throw new BadStateException(
  1851.                 '$sourceVersion',
  1852.                 'Relations of type common can only be added to draft versions'
  1853.             );
  1854.         }
  1855.         if (!$this->permissionResolver->canUser('content''edit'$sourceVersion)) {
  1856.             throw new UnauthorizedException('content''edit', ['contentId' => $sourceVersion->contentInfo->id]);
  1857.         }
  1858.         $spiRelations $this->persistenceHandler->contentHandler()->loadRelations(
  1859.             $sourceVersion->getContentInfo()->id,
  1860.             $sourceVersion->versionNo,
  1861.             APIRelation::COMMON
  1862.         );
  1863.         if (empty($spiRelations)) {
  1864.             throw new InvalidArgumentException(
  1865.                 '$sourceVersion',
  1866.                 'There are no Relations of type COMMON for the given destination'
  1867.             );
  1868.         }
  1869.         // there should be only one relation of type COMMON for each destination,
  1870.         // but in case there were ever more then one, we will remove them all
  1871.         // @todo: alternatively, throw BadStateException?
  1872.         $this->repository->beginTransaction();
  1873.         try {
  1874.             foreach ($spiRelations as $spiRelation) {
  1875.                 if ($spiRelation->destinationContentId == $destinationContent->id) {
  1876.                     $this->persistenceHandler->contentHandler()->removeRelation(
  1877.                         $spiRelation->id,
  1878.                         APIRelation::COMMON
  1879.                     );
  1880.                 }
  1881.             }
  1882.             $this->repository->commit();
  1883.         } catch (Exception $e) {
  1884.             $this->repository->rollback();
  1885.             throw $e;
  1886.         }
  1887.     }
  1888.     /**
  1889.      * Delete Content item Translation from all Versions (including archived ones) of a Content Object.
  1890.      *
  1891.      * NOTE: this operation is risky and permanent, so user interface should provide a warning before performing it.
  1892.      *
  1893.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
  1894.      *         is the Main Translation of a Content Item.
  1895.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
  1896.      *         to delete the content (in one of the locations of the given Content Item).
  1897.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
  1898.      *         is invalid for the given content.
  1899.      *
  1900.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  1901.      * @param string $languageCode
  1902.      *
  1903.      * @since 6.13
  1904.      */
  1905.     public function deleteTranslation(ContentInfo $contentInfostring $languageCode): void
  1906.     {
  1907.         if ($contentInfo->mainLanguageCode === $languageCode) {
  1908.             throw new BadStateException(
  1909.                 '$languageCode',
  1910.                 'The provided translation is the main translation of the Content item'
  1911.             );
  1912.         }
  1913.         $translationWasFound false;
  1914.         $this->repository->beginTransaction();
  1915.         try {
  1916.             $target = (new Target\Builder\VersionBuilder())->translateToAnyLanguageOf([$languageCode])->build();
  1917.             foreach ($this->loadVersions($contentInfo) as $versionInfo) {
  1918.                 if (!$this->permissionResolver->canUser('content''remove'$versionInfo, [$target])) {
  1919.                     throw new UnauthorizedException(
  1920.                         'content',
  1921.                         'remove',
  1922.                         ['contentId' => $contentInfo->id'versionNo' => $versionInfo->versionNo'languageCode' => $languageCode]
  1923.                     );
  1924.                 }
  1925.                 if (!in_array($languageCode$versionInfo->languageCodes)) {
  1926.                     continue;
  1927.                 }
  1928.                 $translationWasFound true;
  1929.                 // If the translation is the version's only one, delete the version
  1930.                 if (count($versionInfo->languageCodes) < 2) {
  1931.                     $this->persistenceHandler->contentHandler()->deleteVersion(
  1932.                         $versionInfo->getContentInfo()->id,
  1933.                         $versionInfo->versionNo
  1934.                     );
  1935.                 }
  1936.             }
  1937.             if (!$translationWasFound) {
  1938.                 throw new InvalidArgumentException(
  1939.                     '$languageCode',
  1940.                     sprintf(
  1941.                         '%s does not exist in the Content item(id=%d)',
  1942.                         $languageCode,
  1943.                         $contentInfo->id
  1944.                     )
  1945.                 );
  1946.             }
  1947.             $this->persistenceHandler->contentHandler()->deleteTranslationFromContent(
  1948.                 $contentInfo->id,
  1949.                 $languageCode
  1950.             );
  1951.             $locationIds array_map(
  1952.                 function (Location $location) {
  1953.                     return $location->id;
  1954.                 },
  1955.                 $this->repository->getLocationService()->loadLocations($contentInfo)
  1956.             );
  1957.             $this->persistenceHandler->urlAliasHandler()->translationRemoved(
  1958.                 $locationIds,
  1959.                 $languageCode
  1960.             );
  1961.             $this->repository->commit();
  1962.         } catch (InvalidArgumentException $e) {
  1963.             $this->repository->rollback();
  1964.             throw $e;
  1965.         } catch (BadStateException $e) {
  1966.             $this->repository->rollback();
  1967.             throw $e;
  1968.         } catch (UnauthorizedException $e) {
  1969.             $this->repository->rollback();
  1970.             throw $e;
  1971.         } catch (Exception $e) {
  1972.             $this->repository->rollback();
  1973.             // cover generic unexpected exception to fulfill API promise on @throws
  1974.             throw new BadStateException('$contentInfo''Translation removal failed'$e);
  1975.         }
  1976.     }
  1977.     /**
  1978.      * Delete specified Translation from a Content Draft.
  1979.      *
  1980.      * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
  1981.      *         is the only one the Content Draft has or it is the main Translation of a Content Object.
  1982.      * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
  1983.      *         to edit the Content (in one of the locations of the given Content Object).
  1984.      * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
  1985.      *         is invalid for the given Draft.
  1986.      * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if specified Version was not found
  1987.      *
  1988.      * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo Content Version Draft
  1989.      * @param string $languageCode Language code of the Translation to be removed
  1990.      *
  1991.      * @return \eZ\Publish\API\Repository\Values\Content\Content Content Draft w/o the specified Translation
  1992.      *
  1993.      * @since 6.12
  1994.      */
  1995.     public function deleteTranslationFromDraft(APIVersionInfo $versionInfostring $languageCode): APIContent
  1996.     {
  1997.         if (!$versionInfo->isDraft()) {
  1998.             throw new BadStateException(
  1999.                 '$versionInfo',
  2000.                 'The version is not a draft, so translations cannot be modified. Create a draft before proceeding'
  2001.             );
  2002.         }
  2003.         if ($versionInfo->contentInfo->mainLanguageCode === $languageCode) {
  2004.             throw new BadStateException(
  2005.                 '$languageCode',
  2006.                 'the specified translation is the main translation of the Content item. Change it before proceeding.'
  2007.             );
  2008.         }
  2009.         if (!$this->permissionResolver->canUser('content''edit'$versionInfo->contentInfo)) {
  2010.             throw new UnauthorizedException(
  2011.                 'content''edit', ['contentId' => $versionInfo->contentInfo->id]
  2012.             );
  2013.         }
  2014.         if (!in_array($languageCode$versionInfo->languageCodes)) {
  2015.             throw new InvalidArgumentException(
  2016.                 '$languageCode',
  2017.                 sprintf(
  2018.                     'The version (ContentId=%d, VersionNo=%d) is not translated into %s',
  2019.                     $versionInfo->contentInfo->id,
  2020.                     $versionInfo->versionNo,
  2021.                     $languageCode
  2022.                 )
  2023.             );
  2024.         }
  2025.         if (count($versionInfo->languageCodes) === 1) {
  2026.             throw new BadStateException(
  2027.                 '$languageCode',
  2028.                 'The provided translation is the only translation in this version'
  2029.             );
  2030.         }
  2031.         $this->repository->beginTransaction();
  2032.         try {
  2033.             $spiContent $this->persistenceHandler->contentHandler()->deleteTranslationFromDraft(
  2034.                 $versionInfo->contentInfo->id,
  2035.                 $versionInfo->versionNo,
  2036.                 $languageCode
  2037.             );
  2038.             $this->repository->commit();
  2039.             return $this->contentDomainMapper->buildContentDomainObject(
  2040.                 $spiContent,
  2041.                 $this->repository->getContentTypeService()->loadContentType(
  2042.                     $spiContent->versionInfo->contentInfo->contentTypeId
  2043.                 )
  2044.             );
  2045.         } catch (APINotFoundException $e) {
  2046.             // avoid wrapping expected NotFoundException in BadStateException handled below
  2047.             $this->repository->rollback();
  2048.             throw $e;
  2049.         } catch (Exception $e) {
  2050.             $this->repository->rollback();
  2051.             // cover generic unexpected exception to fulfill API promise on @throws
  2052.             throw new BadStateException('$contentInfo''Could not remove the translation'$e);
  2053.         }
  2054.     }
  2055.     /**
  2056.      * Hides Content by making all the Locations appear hidden.
  2057.      * It does not persist hidden state on Location object itself.
  2058.      *
  2059.      * Content hidden by this API can be revealed by revealContent API.
  2060.      *
  2061.      * @see revealContent
  2062.      *
  2063.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  2064.      */
  2065.     public function hideContent(ContentInfo $contentInfo): void
  2066.     {
  2067.         if (!$this->permissionResolver->canUser('content''hide'$contentInfo)) {
  2068.             throw new UnauthorizedException('content''hide', ['contentId' => $contentInfo->id]);
  2069.         }
  2070.         $this->repository->beginTransaction();
  2071.         try {
  2072.             $this->persistenceHandler->contentHandler()->updateMetadata(
  2073.                 $contentInfo->id,
  2074.                 new SPIMetadataUpdateStruct([
  2075.                     'isHidden' => true,
  2076.                 ])
  2077.             );
  2078.             $locationHandler $this->persistenceHandler->locationHandler();
  2079.             $childLocations $locationHandler->loadLocationsByContent($contentInfo->id);
  2080.             foreach ($childLocations as $childLocation) {
  2081.                 $locationHandler->setInvisible($childLocation->id);
  2082.             }
  2083.             $this->repository->commit();
  2084.         } catch (Exception $e) {
  2085.             $this->repository->rollback();
  2086.             throw $e;
  2087.         }
  2088.     }
  2089.     /**
  2090.      * Reveals Content hidden by hideContent API.
  2091.      * Locations which were hidden before hiding Content will remain hidden.
  2092.      *
  2093.      * @see hideContent
  2094.      *
  2095.      * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
  2096.      */
  2097.     public function revealContent(ContentInfo $contentInfo): void
  2098.     {
  2099.         if (!$this->permissionResolver->canUser('content''hide'$contentInfo)) {
  2100.             throw new UnauthorizedException('content''hide', ['contentId' => $contentInfo->id]);
  2101.         }
  2102.         $this->repository->beginTransaction();
  2103.         try {
  2104.             $this->persistenceHandler->contentHandler()->updateMetadata(
  2105.                 $contentInfo->id,
  2106.                 new SPIMetadataUpdateStruct([
  2107.                     'isHidden' => false,
  2108.                 ])
  2109.             );
  2110.             $locationHandler $this->persistenceHandler->locationHandler();
  2111.             $childLocations $locationHandler->loadLocationsByContent($contentInfo->id);
  2112.             foreach ($childLocations as $childLocation) {
  2113.                 $locationHandler->setVisible($childLocation->id);
  2114.             }
  2115.             $this->repository->commit();
  2116.         } catch (Exception $e) {
  2117.             $this->repository->rollback();
  2118.             throw $e;
  2119.         }
  2120.     }
  2121.     /**
  2122.      * Instantiates a new content create struct object.
  2123.      *
  2124.      * alwaysAvailable is set to the ContentType's defaultAlwaysAvailable
  2125.      *
  2126.      * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
  2127.      * @param string $mainLanguageCode
  2128.      *
  2129.      * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
  2130.      */
  2131.     public function newContentCreateStruct(ContentType $contentTypestring $mainLanguageCode): APIContentCreateStruct
  2132.     {
  2133.         return new ContentCreateStruct(
  2134.             [
  2135.                 'contentType' => $contentType,
  2136.                 'mainLanguageCode' => $mainLanguageCode,
  2137.                 'alwaysAvailable' => $contentType->defaultAlwaysAvailable,
  2138.             ]
  2139.         );
  2140.     }
  2141.     /**
  2142.      * Instantiates a new content meta data update struct.
  2143.      *
  2144.      * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
  2145.      */
  2146.     public function newContentMetadataUpdateStruct(): ContentMetadataUpdateStruct
  2147.     {
  2148.         return new ContentMetadataUpdateStruct();
  2149.     }
  2150.     /**
  2151.      * Instantiates a new content update struct.
  2152.      *
  2153.      * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
  2154.      */
  2155.     public function newContentUpdateStruct(): APIContentUpdateStruct
  2156.     {
  2157.         return new ContentUpdateStruct();
  2158.     }
  2159.     /**
  2160.      * @param \eZ\Publish\API\Repository\Values\User\User|null $user
  2161.      *
  2162.      * @return \eZ\Publish\API\Repository\Values\User\UserReference
  2163.      */
  2164.     private function resolveUser(?User $user): UserReference
  2165.     {
  2166.         if ($user === null) {
  2167.             $user $this->permissionResolver->getCurrentUserReference();
  2168.         }
  2169.         return $user;
  2170.     }
  2171.     public function validate(
  2172.         ValueObject $object,
  2173.         array $context = [],
  2174.         ?array $fieldIdentifiersToValidate null
  2175.     ): array {
  2176.         return $this->contentValidator->validate(
  2177.             $object,
  2178.             $context,
  2179.             $fieldIdentifiersToValidate
  2180.         );
  2181.     }
  2182.     public function find(Filter $filter, ?array $languages null): ContentList
  2183.     {
  2184.         $filter = clone $filter;
  2185.         if (!empty($languages)) {
  2186.             $filter->andWithCriterion(new LanguageCode($languages));
  2187.         }
  2188.         $permissionCriterion $this->permissionResolver->getQueryPermissionsCriterion();
  2189.         if ($permissionCriterion instanceof Criterion\MatchNone) {
  2190.             return new ContentList(0, []);
  2191.         }
  2192.         if (!$permissionCriterion instanceof Criterion\MatchAll) {
  2193.             if (!$permissionCriterion instanceof FilteringCriterion) {
  2194.                 return new ContentList(0, []);
  2195.             }
  2196.             $filter->andWithCriterion($permissionCriterion);
  2197.         }
  2198.         $contentItems = [];
  2199.         $contentItemsIterator $this->contentFilteringHandler->find($filter);
  2200.         foreach ($contentItemsIterator as $contentItem) {
  2201.             $contentItems[] = $this->contentDomainMapper->buildContentDomainObjectFromPersistence(
  2202.                 $contentItem->content,
  2203.                 $contentItem->type,
  2204.                 $languages,
  2205.             );
  2206.         }
  2207.         return new ContentList($contentItemsIterator->getTotalCount(), $contentItems);
  2208.     }
  2209. }