Completed
Push — ezp-30882-thumbnail-strategy-i... ( de6b29...42b8c0 )
by
unknown
288:43 queued 276:39
created

ContentService::internalLoadContent()   B

Complexity

Conditions 9
Paths 41

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 41
nop 5
dl 0
loc 57
rs 7.3826
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A ContentService::internalLoadContentByRemoteId() 0 27 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
8
9
namespace eZ\Publish\Core\Repository;
10
11
use eZ\Publish\API\Repository\ContentService as ContentServiceInterface;
12
use eZ\Publish\API\Repository\PermissionResolver;
13
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
14
use eZ\Publish\Core\FieldType\FieldTypeRegistry;
15
use eZ\Publish\API\Repository\Values\Content\ContentDraftList;
16
use eZ\Publish\API\Repository\Values\Content\DraftList\Item\ContentDraftListItem;
17
use eZ\Publish\API\Repository\Values\Content\DraftList\Item\UnauthorizedContentDraftListItem;
18
use eZ\Publish\API\Repository\Values\Content\RelationList;
19
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\RelationListItem;
20
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\UnauthorizedRelationListItem;
21
use eZ\Publish\API\Repository\Values\User\UserReference;
22
use eZ\Publish\Core\Repository\Mapper\ContentDomainMapper;
23
use eZ\Publish\Core\Repository\Values\Content\Content;
24
use eZ\Publish\Core\Repository\Values\Content\Location;
25
use eZ\Publish\API\Repository\Values\Content\Language;
26
use eZ\Publish\SPI\Persistence\Handler;
27
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
28
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
29
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
30
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
31
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
32
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
33
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
34
use eZ\Publish\API\Repository\Values\User\User;
35
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
36
use eZ\Publish\API\Repository\Values\Content\Field;
37
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
38
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
39
use eZ\Publish\Core\Base\Exceptions\BadStateException;
40
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
41
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
42
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
43
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
44
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
45
use eZ\Publish\Core\FieldType\ValidationError;
46
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
47
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
48
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
49
use eZ\Publish\SPI\Limitation\Target;
50
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
51
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
52
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
53
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
54
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
55
use eZ\Publish\SPI\Persistence\Content\ContentInfo as SPIContentInfo;
56
use Exception;
57
58
/**
59
 * This class provides service methods for managing content.
60
 */
61
class ContentService implements ContentServiceInterface
62
{
63
    /** @var \eZ\Publish\Core\Repository\Repository */
64
    protected $repository;
65
66
    /** @var \eZ\Publish\SPI\Persistence\Handler */
67
    protected $persistenceHandler;
68
69
    /** @var array */
70
    protected $settings;
71
72
    /** @var \eZ\Publish\Core\Repository\Mapper\ContentDomainMapper */
73
    protected $contentDomainMapper;
74
75
    /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
76
    protected $relationProcessor;
77
78
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
79
    protected $nameSchemaService;
80
81
    /** @var \eZ\Publish\Core\FieldType\FieldTypeRegistry */
82
    protected $fieldTypeRegistry;
83
84
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
85
    private $permissionResolver;
86
87
    public function __construct(
88
        RepositoryInterface $repository,
89
        Handler $handler,
90
        ContentDomainMapper $contentDomainMapper,
91
        Helper\RelationProcessor $relationProcessor,
92
        Helper\NameSchemaService $nameSchemaService,
93
        FieldTypeRegistry $fieldTypeRegistry,
94
        PermissionResolver $permissionResolver,
95
        array $settings = []
96
    ) {
97
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repository of type object<eZ\Publish\API\Repository\Repository> is incompatible with the declared type object<eZ\Publish\Core\Repository\Repository> of property $repository.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
98
        $this->persistenceHandler = $handler;
99
        $this->contentDomainMapper = $contentDomainMapper;
100
        $this->relationProcessor = $relationProcessor;
101
        $this->nameSchemaService = $nameSchemaService;
102
        $this->fieldTypeRegistry = $fieldTypeRegistry;
103
        // Union makes sure default settings are ignored if provided in argument
104
        $this->settings = $settings + [
105
            // Version archive limit (0-50), only enforced on publish, not on un-publish.
106
            'default_version_archive_limit' => 5,
107
        ];
108
        $this->permissionResolver = $permissionResolver;
109
    }
110
111
    /**
112
     * Loads a content info object.
113
     *
114
     * To load fields use loadContent
115
     *
116
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
117
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
118
     *
119
     * @param int $contentId
120
     *
121
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
122
     */
123
    public function loadContentInfo(int $contentId): ContentInfo
124
    {
125
        $contentInfo = $this->internalLoadContentInfoById($contentId);
126
        if (!$this->permissionResolver->canUser('content', 'read', $contentInfo)) {
127
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
128
        }
129
130
        return $contentInfo;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function loadContentInfoList(array $contentIds): iterable
137
    {
138
        $contentInfoList = [];
139
        $spiInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
140
        foreach ($spiInfoList as $id => $spiInfo) {
141
            $contentInfo = $this->contentDomainMapper->buildContentInfoDomainObject($spiInfo);
142
            if ($this->permissionResolver->canUser('content', 'read', $contentInfo)) {
143
                $contentInfoList[$id] = $contentInfo;
144
            }
145
        }
146
147
        return $contentInfoList;
148
    }
149
150
    /**
151
     * Loads a content info object.
152
     *
153
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
154
     *
155
     * @param int $id
156
     *
157
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
158
     */
159
    public function internalLoadContentInfoById(int $id): ContentInfo
160
    {
161
        try {
162
            return $this->contentDomainMapper->buildContentInfoDomainObject(
163
                $this->persistenceHandler->contentHandler()->loadContentInfo($id)
164
            );
165
        } catch (APINotFoundException $e) {
166
            throw new NotFoundException('Content', $id, $e);
167
        }
168
    }
169
170
    /**
171
     * Loads a content info object by remote id.
172
     *
173
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
174
     *
175
     * @param string $remoteId
176
     *
177
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
178
     */
179
    public function internalLoadContentInfoByRemoteId(string $remoteId): ContentInfo
180
    {
181
        try {
182
            return $this->contentDomainMapper->buildContentInfoDomainObject(
183
                $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($remoteId)
184
            );
185
        } catch (APINotFoundException $e) {
186
            throw new NotFoundException('Content', $remoteId, $e);
187
        }
188
    }
189
190
    /**
191
     * Loads a content info object for the given remoteId.
192
     *
193
     * To load fields use loadContent
194
     *
195
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
196
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given remote id does not exist
197
     *
198
     * @param string $remoteId
199
     *
200
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
201
     */
202
    public function loadContentInfoByRemoteId(string $remoteId): ContentInfo
203
    {
204
        $contentInfo = $this->internalLoadContentInfoByRemoteId($remoteId);
205
206
        if (!$this->permissionResolver->canUser('content', 'read', $contentInfo)) {
207
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
208
        }
209
210
        return $contentInfo;
211
    }
212
213
    /**
214
     * Loads a version info of the given content object.
215
     *
216
     * If no version number is given, the method returns the current version
217
     *
218
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
219
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
220
     *
221
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
222
     * @param int|null $versionNo the version number. If not given the current version is returned.
223
     *
224
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
225
     */
226
    public function loadVersionInfo(ContentInfo $contentInfo, ?int $versionNo = null): APIVersionInfo
227
    {
228
        return $this->loadVersionInfoById($contentInfo->id, $versionNo);
229
    }
230
231
    /**
232
     * Loads a version info of the given content object id.
233
     *
234
     * If no version number is given, the method returns the current version
235
     *
236
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
237
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
238
     *
239
     * @param int $contentId
240
     * @param int|null $versionNo the version number. If not given the current version is returned.
241
     *
242
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
243
     */
244
    public function loadVersionInfoById(int $contentId, ?int $versionNo = null): APIVersionInfo
245
    {
246
        try {
247
            $spiVersionInfo = $this->persistenceHandler->contentHandler()->loadVersionInfo(
248
                $contentId,
249
                $versionNo
250
            );
251
        } catch (APINotFoundException $e) {
252
            throw new NotFoundException(
253
                'VersionInfo',
254
                [
255
                    'contentId' => $contentId,
256
                    'versionNo' => $versionNo,
257
                ],
258
                $e
259
            );
260
        }
261
262
        $versionInfo = $this->contentDomainMapper->buildVersionInfoDomainObject($spiVersionInfo);
263
264
        if ($versionInfo->isPublished()) {
265
            $function = 'read';
266
        } else {
267
            $function = 'versionread';
268
        }
269
270
        if (!$this->permissionResolver->canUser('content', $function, $versionInfo)) {
271
            throw new UnauthorizedException('content', $function, ['contentId' => $contentId]);
272
        }
273
274
        return $versionInfo;
275
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280
    public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages = null, ?int $versionNo = null, bool $useAlwaysAvailable = true): APIContent
281
    {
282
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
283
        if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
284
            $useAlwaysAvailable = false;
285
        }
286
287
        return $this->loadContent(
288
            $contentInfo->id,
289
            $languages,
290
            $versionNo,// On purpose pass as-is and not use $contentInfo, to make sure to return actual current version on null
291
            $useAlwaysAvailable
292
        );
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298
    public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages = null, bool $useAlwaysAvailable = true): APIContent
299
    {
300
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
301
        if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
302
            $useAlwaysAvailable = false;
303
        }
304
305
        return $this->loadContent(
306
            $versionInfo->getContentInfo()->id,
307
            $languages,
308
            $versionInfo->versionNo,
309
            $useAlwaysAvailable
310
        );
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316
    public function loadContent(int $contentId, array $languages = null, ?int $versionNo = null, bool $useAlwaysAvailable = true): APIContent
317
    {
318
        $content = $this->internalLoadContentById($contentId, $languages, $versionNo, $useAlwaysAvailable);
319
320
        if (!$this->permissionResolver->canUser('content', 'read', $content)) {
321
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
322
        }
323
        if (
324
            !$content->getVersionInfo()->isPublished()
325
            && !$this->permissionResolver->canUser('content', 'versionread', $content)
326
        ) {
327
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId, 'versionNo' => $versionNo]);
328
        }
329
330
        return $content;
331
    }
332
333
    public function internalLoadContentById(
334
        int $id,
335
        ?array $languages = null,
336
        int $versionNo = null,
337
        bool $useAlwaysAvailable = true
338
    ): APIContent {
339
        try {
340
            $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
341
342
            return $this->internalLoadContentBySPIContentInfo(
343
                $spiContentInfo,
344
                $languages,
345
                $versionNo,
346
                $useAlwaysAvailable
347
            );
348
        } catch (APINotFoundException $e) {
349
            throw new NotFoundException(
350
                'Content',
351
                [
352
                    'id' => $id,
353
                    'languages' => $languages,
354
                    'versionNo' => $versionNo,
355
                ],
356
                $e
357
            );
358
        }
359
    }
360
361
    public function internalLoadContentByRemoteId(
362
        string $remoteId,
363
        array $languages = null,
364
        int $versionNo = null,
365
        bool $useAlwaysAvailable = true
366
    ): APIContent {
367
        try {
368
            $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($remoteId);
369
370
            return $this->internalLoadContentBySPIContentInfo(
371
                $spiContentInfo,
372
                $languages,
373
                $versionNo,
374
                $useAlwaysAvailable
375
            );
376
        } catch (APINotFoundException $e) {
377
            throw new NotFoundException(
378
                'Content',
379
                [
380
                    'remoteId' => $remoteId,
381
                    'languages' => $languages,
382
                    'versionNo' => $versionNo,
383
                ],
384
                $e
385
            );
386
        }
387
    }
388
389
    private function internalLoadContentBySPIContentInfo(SPIContentInfo $spiContentInfo, array $languages = null, int $versionNo = null, bool $useAlwaysAvailable = true): APIContent
390
    {
391
        $loadLanguages = $languages;
392
        $alwaysAvailableLanguageCode = null;
393
        // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
394
        // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
395
        if (!empty($loadLanguages) && $useAlwaysAvailable && $spiContentInfo->alwaysAvailable) {
396
            $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
397
            $loadLanguages = array_unique($loadLanguages);
398
        }
399
400
        $spiContent = $this->persistenceHandler->contentHandler()->load(
401
            $spiContentInfo->id,
402
            $versionNo,
403
            $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 391 can also be of type array; however, eZ\Publish\SPI\Persistence\Content\Handler::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2270
            __METHOD__ . ' is deprecated, use deleteTranslation instead',
2271
            E_USER_DEPRECATED
2272
        );
2273
        $this->deleteTranslation($contentInfo, $languageCode);
2274
    }
2275
2276
    /**
2277
     * Delete Content item Translation from all Versions (including archived ones) of a Content Object.
2278
     *
2279
     * NOTE: this operation is risky and permanent, so user interface should provide a warning before performing it.
2280
     *
2281
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2282
     *         is the Main Translation of a Content Item.
2283
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2284
     *         to delete the content (in one of the locations of the given Content Item).
2285
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2286
     *         is invalid for the given content.
2287
     *
2288
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2289
     * @param string $languageCode
2290
     *
2291
     * @since 6.13
2292
     */
2293
    public function deleteTranslation(ContentInfo $contentInfo, string $languageCode): void
2294
    {
2295
        if ($contentInfo->mainLanguageCode === $languageCode) {
2296
            throw new BadStateException(
2297
                '$languageCode',
2298
                'The provided translation is the main translation of the Content item'
2299
            );
2300
        }
2301
2302
        $translationWasFound = false;
2303
        $this->repository->beginTransaction();
2304
        try {
2305
            foreach ($this->loadVersions($contentInfo) as $versionInfo) {
2306
                if (!$this->permissionResolver->canUser('content', 'remove', $versionInfo)) {
2307
                    throw new UnauthorizedException(
2308
                        'content',
2309
                        'remove',
2310
                        ['contentId' => $contentInfo->id, 'versionNo' => $versionInfo->versionNo]
2311
                    );
2312
                }
2313
2314
                if (!in_array($languageCode, $versionInfo->languageCodes)) {
2315
                    continue;
2316
                }
2317
2318
                $translationWasFound = true;
2319
2320
                // If the translation is the version's only one, delete the version
2321
                if (count($versionInfo->languageCodes) < 2) {
2322
                    $this->persistenceHandler->contentHandler()->deleteVersion(
2323
                        $versionInfo->getContentInfo()->id,
2324
                        $versionInfo->versionNo
2325
                    );
2326
                }
2327
            }
2328
2329
            if (!$translationWasFound) {
2330
                throw new InvalidArgumentException(
2331
                    '$languageCode',
2332
                    sprintf(
2333
                        '%s does not exist in the Content item(id=%d)',
2334
                        $languageCode,
2335
                        $contentInfo->id
2336
                    )
2337
                );
2338
            }
2339
2340
            $this->persistenceHandler->contentHandler()->deleteTranslationFromContent(
2341
                $contentInfo->id,
2342
                $languageCode
2343
            );
2344
            $locationIds = array_map(
2345
                function (Location $location) {
2346
                    return $location->id;
2347
                },
2348
                $this->repository->getLocationService()->loadLocations($contentInfo)
2349
            );
2350
            $this->persistenceHandler->urlAliasHandler()->translationRemoved(
2351
                $locationIds,
2352
                $languageCode
2353
            );
2354
            $this->repository->commit();
2355
        } catch (InvalidArgumentException $e) {
2356
            $this->repository->rollback();
2357
            throw $e;
2358
        } catch (BadStateException $e) {
2359
            $this->repository->rollback();
2360
            throw $e;
2361
        } catch (UnauthorizedException $e) {
2362
            $this->repository->rollback();
2363
            throw $e;
2364
        } catch (Exception $e) {
2365
            $this->repository->rollback();
2366
            // cover generic unexpected exception to fulfill API promise on @throws
2367
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2368
        }
2369
    }
2370
2371
    /**
2372
     * Delete specified Translation from a Content Draft.
2373
     *
2374
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2375
     *         is the only one the Content Draft has or it is the main Translation of a Content Object.
2376
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2377
     *         to edit the Content (in one of the locations of the given Content Object).
2378
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2379
     *         is invalid for the given Draft.
2380
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if specified Version was not found
2381
     *
2382
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo Content Version Draft
2383
     * @param string $languageCode Language code of the Translation to be removed
2384
     *
2385
     * @return \eZ\Publish\API\Repository\Values\Content\Content Content Draft w/o the specified Translation
2386
     *
2387
     * @since 6.12
2388
     */
2389
    public function deleteTranslationFromDraft(APIVersionInfo $versionInfo, string $languageCode): APIContent
2390
    {
2391
        if (!$versionInfo->isDraft()) {
2392
            throw new BadStateException(
2393
                '$versionInfo',
2394
                'The version is not a draft, so translations cannot be modified. Create a draft before proceeding'
2395
            );
2396
        }
2397
2398
        if ($versionInfo->contentInfo->mainLanguageCode === $languageCode) {
2399
            throw new BadStateException(
2400
                '$languageCode',
2401
                'the specified translation is the main translation of the Content item. Change it before proceeding.'
2402
            );
2403
        }
2404
2405
        if (!$this->permissionResolver->canUser('content', 'edit', $versionInfo->contentInfo)) {
2406
            throw new UnauthorizedException(
2407
                'content', 'edit', ['contentId' => $versionInfo->contentInfo->id]
2408
            );
2409
        }
2410
2411
        if (!in_array($languageCode, $versionInfo->languageCodes)) {
2412
            throw new InvalidArgumentException(
2413
                '$languageCode',
2414
                sprintf(
2415
                    'The version (ContentId=%d, VersionNo=%d) is not translated into %s',
2416
                    $versionInfo->contentInfo->id,
2417
                    $versionInfo->versionNo,
2418
                    $languageCode
2419
                )
2420
            );
2421
        }
2422
2423
        if (count($versionInfo->languageCodes) === 1) {
2424
            throw new BadStateException(
2425
                '$languageCode',
2426
                'The provided translation is the only translation in this version'
2427
            );
2428
        }
2429
2430
        $this->repository->beginTransaction();
2431
        try {
2432
            $spiContent = $this->persistenceHandler->contentHandler()->deleteTranslationFromDraft(
2433
                $versionInfo->contentInfo->id,
2434
                $versionInfo->versionNo,
2435
                $languageCode
2436
            );
2437
            $this->repository->commit();
2438
2439
            return $this->contentDomainMapper->buildContentDomainObject(
2440
                $spiContent,
2441
                $this->repository->getContentTypeService()->loadContentType(
2442
                    $spiContent->versionInfo->contentInfo->contentTypeId
2443
                )
2444
            );
2445
        } catch (APINotFoundException $e) {
2446
            // avoid wrapping expected NotFoundException in BadStateException handled below
2447
            $this->repository->rollback();
2448
            throw $e;
2449
        } catch (Exception $e) {
2450
            $this->repository->rollback();
2451
            // cover generic unexpected exception to fulfill API promise on @throws
2452
            throw new BadStateException('$contentInfo', 'Could not remove the translation', $e);
2453
        }
2454
    }
2455
2456
    /**
2457
     * Hides Content by making all the Locations appear hidden.
2458
     * It does not persist hidden state on Location object itself.
2459
     *
2460
     * Content hidden by this API can be revealed by revealContent API.
2461
     *
2462
     * @see revealContent
2463
     *
2464
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2465
     */
2466
    public function hideContent(ContentInfo $contentInfo): void
2467
    {
2468
        if (!$this->permissionResolver->canUser('content', 'hide', $contentInfo)) {
2469
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2470
        }
2471
2472
        $this->repository->beginTransaction();
2473
        try {
2474
            $this->persistenceHandler->contentHandler()->updateMetadata(
2475
                $contentInfo->id,
2476
                new SPIMetadataUpdateStruct([
2477
                    'isHidden' => true,
2478
                ])
2479
            );
2480
            $locationHandler = $this->persistenceHandler->locationHandler();
2481
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2482
            foreach ($childLocations as $childLocation) {
2483
                $locationHandler->setInvisible($childLocation->id);
2484
            }
2485
            $this->repository->commit();
2486
        } catch (Exception $e) {
2487
            $this->repository->rollback();
2488
            throw $e;
2489
        }
2490
    }
2491
2492
    /**
2493
     * Reveals Content hidden by hideContent API.
2494
     * Locations which were hidden before hiding Content will remain hidden.
2495
     *
2496
     * @see hideContent
2497
     *
2498
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2499
     */
2500
    public function revealContent(ContentInfo $contentInfo): void
2501
    {
2502
        if (!$this->permissionResolver->canUser('content', 'hide', $contentInfo)) {
2503
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2504
        }
2505
2506
        $this->repository->beginTransaction();
2507
        try {
2508
            $this->persistenceHandler->contentHandler()->updateMetadata(
2509
                $contentInfo->id,
2510
                new SPIMetadataUpdateStruct([
2511
                    'isHidden' => false,
2512
                ])
2513
            );
2514
            $locationHandler = $this->persistenceHandler->locationHandler();
2515
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2516
            foreach ($childLocations as $childLocation) {
2517
                $locationHandler->setVisible($childLocation->id);
2518
            }
2519
            $this->repository->commit();
2520
        } catch (Exception $e) {
2521
            $this->repository->rollback();
2522
            throw $e;
2523
        }
2524
    }
2525
2526
    /**
2527
     * Instantiates a new content create struct object.
2528
     *
2529
     * alwaysAvailable is set to the ContentType's defaultAlwaysAvailable
2530
     *
2531
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
2532
     * @param string $mainLanguageCode
2533
     *
2534
     * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
2535
     */
2536
    public function newContentCreateStruct(ContentType $contentType, string $mainLanguageCode): APIContentCreateStruct
2537
    {
2538
        return new ContentCreateStruct(
2539
            [
2540
                'contentType' => $contentType,
2541
                'mainLanguageCode' => $mainLanguageCode,
2542
                'alwaysAvailable' => $contentType->defaultAlwaysAvailable,
2543
            ]
2544
        );
2545
    }
2546
2547
    /**
2548
     * Instantiates a new content meta data update struct.
2549
     *
2550
     * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
2551
     */
2552
    public function newContentMetadataUpdateStruct(): ContentMetadataUpdateStruct
2553
    {
2554
        return new ContentMetadataUpdateStruct();
2555
    }
2556
2557
    /**
2558
     * Instantiates a new content update struct.
2559
     *
2560
     * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
2561
     */
2562
    public function newContentUpdateStruct(): APIContentUpdateStruct
2563
    {
2564
        return new ContentUpdateStruct();
2565
    }
2566
2567
    /**
2568
     * @param \eZ\Publish\API\Repository\Values\User\User|null $user
2569
     *
2570
     * @return \eZ\Publish\API\Repository\Values\User\UserReference
2571
     */
2572
    private function resolveUser(?User $user): UserReference
2573
    {
2574
        if ($user === null) {
2575
            $user = $this->permissionResolver->getCurrentUserReference();
2576
        }
2577
2578
        return $user;
2579
    }
2580
}
2581