vendor/ezsystems/ezpublish-kernel/eZ/Publish/Core/Repository/ContentService.php line 393

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