ContentService   F
last analyzed

Complexity

Total Complexity 292

Size/Duplication

Total Lines 2518
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 55

Importance

Changes 0
Metric Value
dl 0
loc 2518
rs 0.8
c 0
b 0
f 0
wmc 292
lcom 1
cbo 55

57 Methods

Rating   Name   Duplication   Size   Complexity  
A loadContentByContentInfo() 0 14 3
A loadVersionInfo() 0 4 1
A loadContentInfoByRemoteId() 0 10 2
A loadContentInfo() 0 9 2
A loadContentInfoList() 0 13 3
A __construct() 0 23 1
A internalLoadContentInfoById() 0 10 2
A internalLoadContentInfoByRemoteId() 0 10 2
A loadVersionInfoById() 0 32 4
A loadContentByVersionInfo() 0 14 3
A loadContent() 0 16 4
A internalLoadContentById() 0 27 2
A internalLoadContentByRemoteId() 0 27 2
A internalLoadContentBySPIContentInfo() 0 31 5
A loadContentByRemoteId() 0 17 4
B loadContentListByContentInfo() 0 41 7
F createContent() 0 202 28
A getDefaultObjectStates() 0 15 3
A getLanguageCodesForCreate() 0 24 5
B mapFieldsForCreate() 0 33 6
A cloneField() 0 15 1
B buildSPILocationCreateStructs() 0 42 5
F updateContentMetadata() 0 87 19
A publishUrlAliasesForContent() 0 26 4
A deleteContent() 0 27 4
C createContentDraft() 0 88 9
A countContentDrafts() 0 10 2
A loadContentDrafts() 0 23 4
A loadContentDraftList() 0 32 5
A updateContent() 0 27 2
F internalUpdateContent() 0 171 28
A getUpdatedLanguageCodes() 0 16 4
A getLanguageCodesForUpdate() 0 12 2
B mapFieldsForUpdate() 0 38 7
A publishVersion() 0 38 4
D copyTranslationsFromPublishedVersion() 0 94 17
B internalPublishVersion() 0 52 6
A getUnixTimestamp() 0 4 1
B deleteVersion() 0 42 6
B loadVersions() 0 29 6
B copyContent() 0 55 6
A loadRelations() 0 14 3
A internalLoadRelations() 0 25 3
A countReverseRelations() 0 10 2
A loadReverseRelations() 0 26 4
A loadReverseRelationList() 0 37 5
A addRelation() 0 41 4
B deleteRelation() 0 50 7
A removeTranslation() 0 8 1
C deleteTranslation() 0 77 11
B deleteTranslationFromDraft() 0 66 8
A hideContent() 0 25 4
A revealContent() 0 25 4
A newContentCreateStruct() 0 10 1
A newContentMetadataUpdateStruct() 0 4 1
A newContentUpdateStruct() 0 4 1
A resolveUser() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like ContentService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ContentService, and based on these observations, apply Extract Interface, too.

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;
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
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
1285
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1286
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
1287
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1288
     */
1289
    public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct): APIContent
1290
    {
1291
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1292
        $content = $this->loadContent(
1293
            $versionInfo->getContentInfo()->id,
1294
            null,
1295
            $versionInfo->versionNo
1296
        );
1297
1298
        if (!$this->repository->getPermissionResolver()->canUser(
1299
            'content',
1300
            'edit',
1301
            $content,
1302
            [
1303
                (new Target\Builder\VersionBuilder())
1304
                    ->updateFieldsTo(
1305
                        $contentUpdateStruct->initialLanguageCode,
1306
                        $contentUpdateStruct->fields
1307
                    )
1308
                    ->build(),
1309
            ]
1310
        )) {
1311
            throw new UnauthorizedException('content', 'edit', ['contentId' => $content->id]);
1312
        }
1313
1314
        return $this->internalUpdateContent($versionInfo, $contentUpdateStruct);
1315
    }
1316
1317
    /**
1318
     * Updates the fields of a draft without checking the permissions.
1319
     *
1320
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
1321
     *                                                                               or if a required field is missing / set to an empty value.
1322
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
1323
     *                                                                          or value is set for non-translatable field in language
1324
     *                                                                          other than main.
1325
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1326
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
1327
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1328
     */
1329
    protected function internalUpdateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct): Content
1330
    {
1331
        $contentUpdateStruct = clone $contentUpdateStruct;
1332
1333
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1334
        $content = $this->internalLoadContentById(
1335
            $versionInfo->getContentInfo()->id,
1336
            null,
1337
            $versionInfo->versionNo
1338
        );
1339
        if (!$content->versionInfo->isDraft()) {
1340
            throw new BadStateException(
1341
                '$versionInfo',
1342
                'The version is not a draft and cannot be updated'
1343
            );
1344
        }
1345
1346
        $mainLanguageCode = $content->contentInfo->mainLanguageCode;
1347
        if ($contentUpdateStruct->initialLanguageCode === null) {
1348
            $contentUpdateStruct->initialLanguageCode = $mainLanguageCode;
1349
        }
1350
1351
        $allLanguageCodes = $this->getLanguageCodesForUpdate($contentUpdateStruct, $content);
1352
        $contentLanguageHandler = $this->persistenceHandler->contentLanguageHandler();
1353
        foreach ($allLanguageCodes as $languageCode) {
1354
            $contentLanguageHandler->loadByLanguageCode($languageCode);
1355
        }
1356
1357
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1358
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1359
            $content->contentInfo->contentTypeId
1360
        );
1361
        $fields = $this->mapFieldsForUpdate(
1362
            $contentUpdateStruct,
1363
            $contentType,
1364
            $mainLanguageCode
1365
        );
1366
1367
        $fieldValues = [];
1368
        $spiFields = [];
1369
        $allFieldErrors = [];
1370
        $inputRelations = [];
1371
        $locationIdToContentIdMapping = [];
1372
1373
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1374
            /** @var $fieldType \eZ\Publish\SPI\FieldType\FieldType */
1375
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1376
                $fieldDefinition->fieldTypeIdentifier
1377
            );
1378
1379
            foreach ($allLanguageCodes as $languageCode) {
1380
                $isCopied = $isEmpty = $isRetained = false;
1381
                $isLanguageNew = !in_array($languageCode, $content->versionInfo->languageCodes);
1382
                $isLanguageUpdated = in_array($languageCode, $updatedLanguageCodes);
1383
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $mainLanguageCode;
1384
                $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
1385
                $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
1386
1387
                if (!$isFieldUpdated && !$isLanguageNew) {
1388
                    $isRetained = true;
1389
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1390
                } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
1391
                    $isCopied = true;
1392
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1393
                } elseif ($isFieldUpdated) {
1394
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
1395
                } else {
1396
                    $fieldValue = $fieldDefinition->defaultValue;
1397
                }
1398
1399
                $fieldValue = $fieldType->acceptValue($fieldValue);
1400
1401
                if ($fieldType->isEmptyValue($fieldValue)) {
1402
                    $isEmpty = true;
1403
                    if ($isLanguageUpdated && $fieldDefinition->isRequired) {
1404
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
1405
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
1406
                            null,
1407
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
1408
                            'empty'
1409
                        );
1410
                    }
1411
                } elseif ($isLanguageUpdated) {
1412
                    $fieldErrors = $fieldType->validate(
1413
                        $fieldDefinition,
1414
                        $fieldValue
1415
                    );
1416
                    if (!empty($fieldErrors)) {
1417
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
1418
                    }
1419
                }
1420
1421
                if (!empty($allFieldErrors)) {
1422
                    continue;
1423
                }
1424
1425
                $this->relationProcessor->appendFieldRelations(
1426
                    $inputRelations,
1427
                    $locationIdToContentIdMapping,
1428
                    $fieldType,
1429
                    $fieldValue,
1430
                    $fieldDefinition->id
1431
                );
1432
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
1433
1434
                if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
1435
                    continue;
1436
                }
1437
1438
                $spiFields[] = new SPIField(
1439
                    [
1440
                        'id' => $isLanguageNew ?
1441
                            null :
1442
                            $content->getField($fieldDefinition->identifier, $languageCode)->id,
1443
                        'fieldDefinitionId' => $fieldDefinition->id,
1444
                        'type' => $fieldDefinition->fieldTypeIdentifier,
1445
                        'value' => $fieldType->toPersistenceValue($fieldValue),
1446
                        'languageCode' => $languageCode,
1447
                        'versionNo' => $versionInfo->versionNo,
1448
                    ]
1449
                );
1450
            }
1451
        }
1452
1453
        if (!empty($allFieldErrors)) {
1454
            throw new ContentFieldValidationException($allFieldErrors);
1455
        }
1456
1457
        $spiContentUpdateStruct = new SPIContentUpdateStruct(
1458
            [
1459
                'name' => $this->nameSchemaService->resolveNameSchema(
1460
                    $content,
1461
                    $fieldValues,
1462
                    $allLanguageCodes,
1463
                    $contentType
1464
                ),
1465
                'creatorId' => $contentUpdateStruct->creatorId ?: $this->permissionResolver->getCurrentUserReference()->getUserId(),
1466
                'fields' => $spiFields,
1467
                'modificationDate' => time(),
1468
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1469
                    $contentUpdateStruct->initialLanguageCode
1470
                )->id,
1471
            ]
1472
        );
1473
        $existingRelations = $this->internalLoadRelations($versionInfo);
1474
1475
        $this->repository->beginTransaction();
1476
        try {
1477
            $spiContent = $this->persistenceHandler->contentHandler()->updateContent(
1478
                $versionInfo->getContentInfo()->id,
1479
                $versionInfo->versionNo,
1480
                $spiContentUpdateStruct
1481
            );
1482
            $this->relationProcessor->processFieldRelations(
1483
                $inputRelations,
1484
                $spiContent->versionInfo->contentInfo->id,
1485
                $spiContent->versionInfo->versionNo,
1486
                $contentType,
1487
                $existingRelations
1488
            );
1489
            $this->repository->commit();
1490
        } catch (Exception $e) {
1491
            $this->repository->rollback();
1492
            throw $e;
1493
        }
1494
1495
        return $this->contentDomainMapper->buildContentDomainObject(
1496
            $spiContent,
1497
            $contentType
1498
        );
1499
    }
1500
1501
    /**
1502
     * Returns only updated language codes.
1503
     *
1504
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1505
     *
1506
     * @return array
1507
     */
1508
    private function getUpdatedLanguageCodes(APIContentUpdateStruct $contentUpdateStruct): array
1509
    {
1510
        $languageCodes = [
1511
            $contentUpdateStruct->initialLanguageCode => true,
1512
        ];
1513
1514
        foreach ($contentUpdateStruct->fields as $field) {
1515
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
1516
                continue;
1517
            }
1518
1519
            $languageCodes[$field->languageCode] = true;
1520
        }
1521
1522
        return array_keys($languageCodes);
1523
    }
1524
1525
    /**
1526
     * Returns all language codes used in given $fields.
1527
     *
1528
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value exists in initial language
1529
     *
1530
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1531
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1532
     *
1533
     * @return array
1534
     */
1535
    protected function getLanguageCodesForUpdate(APIContentUpdateStruct $contentUpdateStruct, APIContent $content): array
1536
    {
1537
        $languageCodes = array_fill_keys($content->versionInfo->languageCodes, true);
1538
        $languageCodes[$contentUpdateStruct->initialLanguageCode] = true;
1539
1540
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1541
        foreach ($updatedLanguageCodes as $languageCode) {
1542
            $languageCodes[$languageCode] = true;
1543
        }
1544
1545
        return array_keys($languageCodes);
1546
    }
1547
1548
    /**
1549
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
1550
     *
1551
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
1552
     *                                                                          or value is set for non-translatable field in language
1553
     *                                                                          other than main
1554
     *
1555
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1556
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1557
     * @param string $mainLanguageCode
1558
     *
1559
     * @return array
1560
     */
1561
    protected function mapFieldsForUpdate(
1562
        APIContentUpdateStruct $contentUpdateStruct,
1563
        ContentType $contentType,
1564
        string $mainLanguageCode
1565
    ): array {
1566
        $fields = [];
1567
1568
        foreach ($contentUpdateStruct->fields as $field) {
1569
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1570
1571
            if ($fieldDefinition === null) {
1572
                throw new ContentValidationException(
1573
                    "Field definition '%identifier%' does not exist in given Content Type",
1574
                    ['%identifier%' => $field->fieldDefIdentifier]
1575
                );
1576
            }
1577
1578
            if ($field->languageCode === null) {
1579
                if ($fieldDefinition->isTranslatable) {
1580
                    $languageCode = $contentUpdateStruct->initialLanguageCode;
1581
                } else {
1582
                    $languageCode = $mainLanguageCode;
1583
                }
1584
                $field = $this->cloneField($field, ['languageCode' => $languageCode]);
1585
            }
1586
1587
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $mainLanguageCode)) {
1588
                throw new ContentValidationException(
1589
                    "You cannot set a value for the non-translatable Field definition '%identifier%' in language '%languageCode%'",
1590
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
1591
                );
1592
            }
1593
1594
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
1595
        }
1596
1597
        return $fields;
1598
    }
1599
1600
    /**
1601
     * Publishes a content version.
1602
     *
1603
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1604
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1605
     *
1606
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1607
     * @param string[] $translations
1608
     *
1609
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1610
     *
1611
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1612
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1613
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1614
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1615
     */
1616
    public function publishVersion(APIVersionInfo $versionInfo, array $translations = Language::ALL): APIContent
1617
    {
1618
        $content = $this->internalLoadContentById(
1619
            $versionInfo->contentInfo->id,
1620
            null,
1621
            $versionInfo->versionNo
1622
        );
1623
1624
        $targets = [];
1625
        if (!empty($translations)) {
1626
            $targets[] = (new Target\Builder\VersionBuilder())
1627
                ->publishTranslations($translations)
1628
                ->build();
1629
        }
1630
1631
        if (!$this->permissionResolver->canUser(
1632
            'content',
1633
            'publish',
1634
            $content,
1635
            $targets
1636
        )) {
1637
            throw new UnauthorizedException(
1638
                'content', 'publish', ['contentId' => $content->id]
1639
            );
1640
        }
1641
1642
        $this->repository->beginTransaction();
1643
        try {
1644
            $this->copyTranslationsFromPublishedVersion($content->versionInfo, $translations);
1645
            $content = $this->internalPublishVersion($content->getVersionInfo(), null);
1646
            $this->repository->commit();
1647
        } catch (Exception $e) {
1648
            $this->repository->rollback();
1649
            throw $e;
1650
        }
1651
1652
        return $content;
1653
    }
1654
1655
    /**
1656
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1657
     * @param array $translations
1658
     *
1659
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1660
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException
1661
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
1662
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1663
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1664
     */
1665
    protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionInfo, array $translations = []): void
1666
    {
1667
        $contendId = $versionInfo->contentInfo->id;
1668
1669
        $currentContent = $this->internalLoadContentById($contendId);
1670
        $currentVersionInfo = $currentContent->versionInfo;
1671
1672
        // Copying occurs only if:
1673
        // - There is published Version
1674
        // - Published version is older than the currently published one unless specific translations are provided.
1675
        if (!$currentVersionInfo->isPublished() ||
1676
            ($versionInfo->versionNo >= $currentVersionInfo->versionNo && empty($translations))) {
1677
            return;
1678
        }
1679
1680
        if (empty($translations)) {
1681
            $languagesToCopy = array_diff(
1682
                $currentVersionInfo->languageCodes,
1683
                $versionInfo->languageCodes
1684
            );
1685
        } else {
1686
            $languagesToCopy = array_diff(
1687
                $currentVersionInfo->languageCodes,
1688
                $translations
1689
            );
1690
        }
1691
1692
        if (empty($languagesToCopy)) {
1693
            return;
1694
        }
1695
1696
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1697
            $currentVersionInfo->contentInfo->contentTypeId
1698
        );
1699
1700
        // Find only translatable fields to update with selected languages
1701
        $updateStruct = $this->newContentUpdateStruct();
1702
        $updateStruct->initialLanguageCode = $versionInfo->initialLanguageCode;
1703
1704
        $contentToPublish = $this->internalLoadContentById($contendId, null, $versionInfo->versionNo);
1705
        $fallbackUpdateStruct = $this->newContentUpdateStruct();
1706
1707
        foreach ($currentContent->getFields() as $field) {
1708
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1709
1710
            if (!$fieldDefinition->isTranslatable || !\in_array($field->languageCode, $languagesToCopy)) {
1711
                continue;
1712
            }
1713
1714
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1715
                $fieldDefinition->fieldTypeIdentifier
1716
            );
1717
1718
            $newValue = $contentToPublish->getFieldValue(
1719
                $fieldDefinition->identifier,
1720
                $field->languageCode
1721
            );
1722
1723
            $value = $field->value;
1724
            if ($fieldDefinition->isRequired && $fieldType->isEmptyValue($value)) {
1725
                if (!$fieldType->isEmptyValue($fieldDefinition->defaultValue)) {
1726
                    $value = $fieldDefinition->defaultValue;
1727
                } else {
1728
                    $value = $contentToPublish->getFieldValue($field->fieldDefIdentifier, $versionInfo->initialLanguageCode);
1729
                }
1730
                $fallbackUpdateStruct->setField(
1731
                    $field->fieldDefIdentifier,
1732
                    $value,
1733
                    $field->languageCode
1734
                );
1735
                continue;
1736
            }
1737
1738
            if ($newValue !== null
1739
                && $field->value !== null
1740
                && $fieldType->toHash($newValue) === $fieldType->toHash($field->value)) {
1741
                continue;
1742
            }
1743
1744
            $updateStruct->setField($field->fieldDefIdentifier, $value, $field->languageCode);
1745
        }
1746
1747
        // Nothing to copy, skip update
1748
        if (empty($updateStruct->fields)) {
1749
            return;
1750
        }
1751
1752
        // Do fallback only if content needs to be updated
1753
        foreach ($fallbackUpdateStruct->fields as $fallbackField) {
1754
            $updateStruct->setField($fallbackField->fieldDefIdentifier, $fallbackField->value, $fallbackField->languageCode);
1755
        }
1756
1757
        $this->internalUpdateContent($versionInfo, $updateStruct);
1758
    }
1759
1760
    /**
1761
     * Publishes a content version.
1762
     *
1763
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1764
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1765
     *
1766
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1767
     *
1768
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1769
     * @param int|null $publicationDate If null existing date is kept if there is one, otherwise current time is used.
1770
     *
1771
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1772
     */
1773
    protected function internalPublishVersion(APIVersionInfo $versionInfo, $publicationDate = null)
1774
    {
1775
        if (!$versionInfo->isDraft()) {
1776
            throw new BadStateException('$versionInfo', 'Only versions in draft status can be published.');
1777
        }
1778
1779
        $currentTime = $this->getUnixTimestamp();
1780
        if ($publicationDate === null && $versionInfo->versionNo === 1) {
1781
            $publicationDate = $currentTime;
1782
        }
1783
1784
        $contentInfo = $versionInfo->getContentInfo();
1785
        $metadataUpdateStruct = new SPIMetadataUpdateStruct();
1786
        $metadataUpdateStruct->publicationDate = $publicationDate;
1787
        $metadataUpdateStruct->modificationDate = $currentTime;
1788
        $metadataUpdateStruct->isHidden = $contentInfo->isHidden;
1789
1790
        $contentId = $contentInfo->id;
1791
        $spiContent = $this->persistenceHandler->contentHandler()->publish(
1792
            $contentId,
1793
            $versionInfo->versionNo,
1794
            $metadataUpdateStruct
1795
        );
1796
1797
        $content = $this->contentDomainMapper->buildContentDomainObject(
1798
            $spiContent,
1799
            $this->repository->getContentTypeService()->loadContentType(
1800
                $spiContent->versionInfo->contentInfo->contentTypeId
1801
            )
1802
        );
1803
1804
        $this->publishUrlAliasesForContent($content);
1805
1806
        // Delete version archive overflow if any, limit is 0-50 (however 0 will mean 1 if content is unpublished)
1807
        $archiveList = $this->persistenceHandler->contentHandler()->listVersions(
1808
            $contentId,
1809
            APIVersionInfo::STATUS_ARCHIVED,
1810
            100 // Limited to avoid publishing taking to long, besides SE limitations this is why limit is max 50
1811
        );
1812
1813
        $maxVersionArchiveCount = max(0, min(50, $this->settings['default_version_archive_limit']));
1814
        while (!empty($archiveList) && count($archiveList) > $maxVersionArchiveCount) {
1815
            /** @var \eZ\Publish\SPI\Persistence\Content\VersionInfo $archiveVersion */
1816
            $archiveVersion = array_shift($archiveList);
1817
            $this->persistenceHandler->contentHandler()->deleteVersion(
1818
                $contentId,
1819
                $archiveVersion->versionNo
1820
            );
1821
        }
1822
1823
        return $content;
1824
    }
1825
1826
    /**
1827
     * @return int
1828
     */
1829
    protected function getUnixTimestamp(): int
1830
    {
1831
        return time();
1832
    }
1833
1834
    /**
1835
     * Removes the given version.
1836
     *
1837
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is in
1838
     *         published state or is a last version of Content in non draft state
1839
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to remove this version
1840
     *
1841
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1842
     */
1843
    public function deleteVersion(APIVersionInfo $versionInfo): void
1844
    {
1845
        if ($versionInfo->isPublished()) {
1846
            throw new BadStateException(
1847
                '$versionInfo',
1848
                'The Version is published and cannot be removed'
1849
            );
1850
        }
1851
1852
        if (!$this->permissionResolver->canUser('content', 'versionremove', $versionInfo)) {
1853
            throw new UnauthorizedException(
1854
                'content',
1855
                'versionremove',
1856
                ['contentId' => $versionInfo->contentInfo->id, 'versionNo' => $versionInfo->versionNo]
1857
            );
1858
        }
1859
1860
        $versionList = $this->persistenceHandler->contentHandler()->listVersions(
1861
            $versionInfo->contentInfo->id,
1862
            null,
1863
            2
1864
        );
1865
1866
        if (count($versionList) === 1 && !$versionInfo->isDraft()) {
1867
            throw new BadStateException(
1868
                '$versionInfo',
1869
                'The Version is the last version of the Content item and cannot be removed'
1870
            );
1871
        }
1872
1873
        $this->repository->beginTransaction();
1874
        try {
1875
            $this->persistenceHandler->contentHandler()->deleteVersion(
1876
                $versionInfo->getContentInfo()->id,
1877
                $versionInfo->versionNo
1878
            );
1879
            $this->repository->commit();
1880
        } catch (Exception $e) {
1881
            $this->repository->rollback();
1882
            throw $e;
1883
        }
1884
    }
1885
1886
    /**
1887
     * Loads all versions for the given content.
1888
     *
1889
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to list versions
1890
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the given status is invalid
1891
     *
1892
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1893
     * @param int|null $status
1894
     *
1895
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Sorted by creation date
1896
     */
1897
    public function loadVersions(ContentInfo $contentInfo, ?int $status = null): iterable
1898
    {
1899
        if (!$this->permissionResolver->canUser('content', 'versionread', $contentInfo)) {
1900
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentInfo->id]);
1901
        }
1902
1903
        if ($status !== null && !in_array((int)$status, [VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED], true)) {
1904
            throw new InvalidArgumentException(
1905
                'status',
1906
                sprintf(
1907
                    'available statuses are: %d (draft), %d (published), %d (archived), %d given',
1908
                    VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED, $status
1909
                ));
1910
        }
1911
1912
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->listVersions($contentInfo->id, $status);
1913
1914
        $versions = [];
1915
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1916
            $versionInfo = $this->contentDomainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1917
            if (!$this->permissionResolver->canUser('content', 'versionread', $versionInfo)) {
1918
                throw new UnauthorizedException('content', 'versionread', ['versionId' => $versionInfo->id]);
1919
            }
1920
1921
            $versions[] = $versionInfo;
1922
        }
1923
1924
        return $versions;
1925
    }
1926
1927
    /**
1928
     * Copies the content to a new location. If no version is given,
1929
     * all versions are copied, otherwise only the given version.
1930
     *
1931
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to copy the content to the given location
1932
     *
1933
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1934
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $destinationLocationCreateStruct the target location where the content is copied to
1935
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1936
     *
1937
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1938
     */
1939
    public function copyContent(ContentInfo $contentInfo, LocationCreateStruct $destinationLocationCreateStruct, ?APIVersionInfo $versionInfo = null): APIContent
1940
    {
1941
        $destinationLocation = $this->repository->getLocationService()->loadLocation(
1942
            $destinationLocationCreateStruct->parentLocationId
1943
        );
1944
        if (!$this->permissionResolver->canUser('content', 'create', $contentInfo, [$destinationLocation])) {
1945
            throw new UnauthorizedException(
1946
                'content',
1947
                'create',
1948
                [
1949
                    'parentLocationId' => $destinationLocationCreateStruct->parentLocationId,
1950
                    'sectionId' => $contentInfo->sectionId,
1951
                ]
1952
            );
1953
        }
1954
        if (!$this->permissionResolver->canUser('content', 'manage_locations', $contentInfo, [$destinationLocation])) {
1955
            throw new UnauthorizedException('content', 'manage_locations', ['contentId' => $contentInfo->id]);
1956
        }
1957
1958
        $defaultObjectStates = $this->getDefaultObjectStates();
1959
1960
        $this->repository->beginTransaction();
1961
        try {
1962
            $spiContent = $this->persistenceHandler->contentHandler()->copy(
1963
                $contentInfo->id,
1964
                $versionInfo ? $versionInfo->versionNo : null,
1965
                $this->permissionResolver->getCurrentUserReference()->getUserId()
1966
            );
1967
1968
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
1969
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
1970
                $objectStateHandler->setContentState(
1971
                    $spiContent->versionInfo->contentInfo->id,
1972
                    $objectStateGroupId,
1973
                    $objectState->id
1974
                );
1975
            }
1976
1977
            $content = $this->internalPublishVersion(
1978
                $this->contentDomainMapper->buildVersionInfoDomainObject($spiContent->versionInfo),
1979
                $spiContent->versionInfo->creationDate
1980
            );
1981
1982
            $this->repository->getLocationService()->createLocation(
1983
                $content->getVersionInfo()->getContentInfo(),
1984
                $destinationLocationCreateStruct
1985
            );
1986
            $this->repository->commit();
1987
        } catch (Exception $e) {
1988
            $this->repository->rollback();
1989
            throw $e;
1990
        }
1991
1992
        return $this->internalLoadContentById($content->id);
1993
    }
1994
1995
    /**
1996
     * Loads all outgoing relations for the given version.
1997
     *
1998
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1999
     *
2000
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
2001
     *
2002
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
2003
     */
2004
    public function loadRelations(APIVersionInfo $versionInfo): iterable
2005
    {
2006
        if ($versionInfo->isPublished()) {
2007
            $function = 'read';
2008
        } else {
2009
            $function = 'versionread';
2010
        }
2011
2012
        if (!$this->permissionResolver->canUser('content', $function, $versionInfo)) {
2013
            throw new UnauthorizedException('content', $function);
2014
        }
2015
2016
        return $this->internalLoadRelations($versionInfo);
2017
    }
2018
2019
    /**
2020
     * Loads all outgoing relations for the given version without checking the permissions.
2021
     *
2022
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
2023
     *
2024
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
2025
     */
2026
    protected function internalLoadRelations(APIVersionInfo $versionInfo): array
2027
    {
2028
        $contentInfo = $versionInfo->getContentInfo();
2029
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
2030
            $contentInfo->id,
2031
            $versionInfo->versionNo
2032
        );
2033
2034
        /** @var $relations \eZ\Publish\API\Repository\Values\Content\Relation[] */
2035
        $relations = [];
2036
        foreach ($spiRelations as $spiRelation) {
2037
            $destinationContentInfo = $this->internalLoadContentInfoById($spiRelation->destinationContentId);
2038
            if (!$this->permissionResolver->canUser('content', 'read', $destinationContentInfo)) {
2039
                continue;
2040
            }
2041
2042
            $relations[] = $this->contentDomainMapper->buildRelationDomainObject(
2043
                $spiRelation,
2044
                $contentInfo,
2045
                $destinationContentInfo
2046
            );
2047
        }
2048
2049
        return $relations;
2050
    }
2051
2052
    /**
2053
     * {@inheritdoc}
2054
     */
2055
    public function countReverseRelations(ContentInfo $contentInfo): int
2056
    {
2057
        if (!$this->permissionResolver->canUser('content', 'reverserelatedlist', $contentInfo)) {
2058
            return 0;
2059
        }
2060
2061
        return $this->persistenceHandler->contentHandler()->countReverseRelations(
2062
            $contentInfo->id
2063
        );
2064
    }
2065
2066
    /**
2067
     * Loads all incoming relations for a content object.
2068
     *
2069
     * The relations come only from published versions of the source content objects
2070
     *
2071
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
2072
     *
2073
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2074
     *
2075
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
2076
     */
2077
    public function loadReverseRelations(ContentInfo $contentInfo): iterable
2078
    {
2079
        if (!$this->permissionResolver->canUser('content', 'reverserelatedlist', $contentInfo)) {
2080
            throw new UnauthorizedException('content', 'reverserelatedlist', ['contentId' => $contentInfo->id]);
2081
        }
2082
2083
        $spiRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations(
2084
            $contentInfo->id
2085
        );
2086
2087
        $returnArray = [];
2088
        foreach ($spiRelations as $spiRelation) {
2089
            $sourceContentInfo = $this->internalLoadContentInfoById($spiRelation->sourceContentId);
2090
            if (!$this->permissionResolver->canUser('content', 'read', $sourceContentInfo)) {
2091
                continue;
2092
            }
2093
2094
            $returnArray[] = $this->contentDomainMapper->buildRelationDomainObject(
2095
                $spiRelation,
2096
                $sourceContentInfo,
2097
                $contentInfo
2098
            );
2099
        }
2100
2101
        return $returnArray;
2102
    }
2103
2104
    /**
2105
     * {@inheritdoc}
2106
     */
2107
    public function loadReverseRelationList(ContentInfo $contentInfo, int $offset = 0, int $limit = -1): RelationList
2108
    {
2109
        $list = new RelationList();
2110
        if (!$this->repository->getPermissionResolver()->canUser('content', 'reverserelatedlist', $contentInfo)) {
2111
            return $list;
2112
        }
2113
2114
        $list->totalCount = $this->persistenceHandler->contentHandler()->countReverseRelations(
2115
            $contentInfo->id
2116
        );
2117
        if ($list->totalCount > 0) {
2118
            $spiRelationList = $this->persistenceHandler->contentHandler()->loadReverseRelationList(
2119
                $contentInfo->id,
2120
                $offset,
2121
                $limit
2122
            );
2123
            foreach ($spiRelationList as $spiRelation) {
2124
                $sourceContentInfo = $this->internalLoadContentInfoById($spiRelation->sourceContentId);
2125
                if ($this->repository->getPermissionResolver()->canUser('content', 'read', $sourceContentInfo)) {
2126
                    $relation = $this->contentDomainMapper->buildRelationDomainObject(
2127
                        $spiRelation,
2128
                        $sourceContentInfo,
2129
                        $contentInfo
2130
                    );
2131
                    $list->items[] = new RelationListItem($relation);
2132
                } else {
2133
                    $list->items[] = new UnauthorizedRelationListItem(
2134
                        'content',
2135
                        'read',
2136
                        ['contentId' => $sourceContentInfo->id]
2137
                    );
2138
                }
2139
            }
2140
        }
2141
2142
        return $list;
2143
    }
2144
2145
    /**
2146
     * Adds a relation of type common.
2147
     *
2148
     * The source of the relation is the content and version
2149
     * referenced by $versionInfo.
2150
     *
2151
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to edit this version
2152
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2153
     *
2154
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2155
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent the destination of the relation
2156
     *
2157
     * @return \eZ\Publish\API\Repository\Values\Content\Relation the newly created relation
2158
     */
2159
    public function addRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent): APIRelation
2160
    {
2161
        $sourceVersion = $this->loadVersionInfoById(
2162
            $sourceVersion->contentInfo->id,
2163
            $sourceVersion->versionNo
2164
        );
2165
2166
        if (!$sourceVersion->isDraft()) {
2167
            throw new BadStateException(
2168
                '$sourceVersion',
2169
                'Relations of type common can only be added to draft versions'
2170
            );
2171
        }
2172
2173
        if (!$this->permissionResolver->canUser('content', 'edit', $sourceVersion)) {
2174
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2175
        }
2176
2177
        $sourceContentInfo = $sourceVersion->getContentInfo();
2178
2179
        $this->repository->beginTransaction();
2180
        try {
2181
            $spiRelation = $this->persistenceHandler->contentHandler()->addRelation(
2182
                new SPIRelationCreateStruct(
2183
                    [
2184
                        'sourceContentId' => $sourceContentInfo->id,
2185
                        'sourceContentVersionNo' => $sourceVersion->versionNo,
2186
                        'sourceFieldDefinitionId' => null,
2187
                        'destinationContentId' => $destinationContent->id,
2188
                        'type' => APIRelation::COMMON,
2189
                    ]
2190
                )
2191
            );
2192
            $this->repository->commit();
2193
        } catch (Exception $e) {
2194
            $this->repository->rollback();
2195
            throw $e;
2196
        }
2197
2198
        return $this->contentDomainMapper->buildRelationDomainObject($spiRelation, $sourceContentInfo, $destinationContent);
2199
    }
2200
2201
    /**
2202
     * Removes a relation of type COMMON from a draft.
2203
     *
2204
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed edit this version
2205
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2206
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is no relation of type COMMON for the given destination
2207
     *
2208
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2209
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent
2210
     */
2211
    public function deleteRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent): void
2212
    {
2213
        $sourceVersion = $this->loadVersionInfoById(
2214
            $sourceVersion->contentInfo->id,
2215
            $sourceVersion->versionNo
2216
        );
2217
2218
        if (!$sourceVersion->isDraft()) {
2219
            throw new BadStateException(
2220
                '$sourceVersion',
2221
                'Relations of type common can only be added to draft versions'
2222
            );
2223
        }
2224
2225
        if (!$this->permissionResolver->canUser('content', 'edit', $sourceVersion)) {
2226
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2227
        }
2228
2229
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
2230
            $sourceVersion->getContentInfo()->id,
2231
            $sourceVersion->versionNo,
2232
            APIRelation::COMMON
2233
        );
2234
2235
        if (empty($spiRelations)) {
2236
            throw new InvalidArgumentException(
2237
                '$sourceVersion',
2238
                'There are no Relations of type COMMON for the given destination'
2239
            );
2240
        }
2241
2242
        // there should be only one relation of type COMMON for each destination,
2243
        // but in case there were ever more then one, we will remove them all
2244
        // @todo: alternatively, throw BadStateException?
2245
        $this->repository->beginTransaction();
2246
        try {
2247
            foreach ($spiRelations as $spiRelation) {
2248
                if ($spiRelation->destinationContentId == $destinationContent->id) {
2249
                    $this->persistenceHandler->contentHandler()->removeRelation(
2250
                        $spiRelation->id,
2251
                        APIRelation::COMMON
2252
                    );
2253
                }
2254
            }
2255
            $this->repository->commit();
2256
        } catch (Exception $e) {
2257
            $this->repository->rollback();
2258
            throw $e;
2259
        }
2260
    }
2261
2262
    /**
2263
     * {@inheritdoc}
2264
     */
2265
    public function removeTranslation(ContentInfo $contentInfo, string $languageCode): void
2266
    {
2267
        @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...
2268
            __METHOD__ . ' is deprecated, use deleteTranslation instead',
2269
            E_USER_DEPRECATED
2270
        );
2271
        $this->deleteTranslation($contentInfo, $languageCode);
2272
    }
2273
2274
    /**
2275
     * Delete Content item Translation from all Versions (including archived ones) of a Content Object.
2276
     *
2277
     * NOTE: this operation is risky and permanent, so user interface should provide a warning before performing it.
2278
     *
2279
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2280
     *         is the Main Translation of a Content Item.
2281
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2282
     *         to delete the content (in one of the locations of the given Content Item).
2283
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2284
     *         is invalid for the given content.
2285
     *
2286
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2287
     * @param string $languageCode
2288
     *
2289
     * @since 6.13
2290
     */
2291
    public function deleteTranslation(ContentInfo $contentInfo, string $languageCode): void
2292
    {
2293
        if ($contentInfo->mainLanguageCode === $languageCode) {
2294
            throw new BadStateException(
2295
                '$languageCode',
2296
                'The provided translation is the main translation of the Content item'
2297
            );
2298
        }
2299
2300
        $translationWasFound = false;
2301
        $this->repository->beginTransaction();
2302
        try {
2303
            foreach ($this->loadVersions($contentInfo) as $versionInfo) {
2304
                if (!$this->permissionResolver->canUser('content', 'remove', $versionInfo)) {
2305
                    throw new UnauthorizedException(
2306
                        'content',
2307
                        'remove',
2308
                        ['contentId' => $contentInfo->id, 'versionNo' => $versionInfo->versionNo]
2309
                    );
2310
                }
2311
2312
                if (!in_array($languageCode, $versionInfo->languageCodes)) {
2313
                    continue;
2314
                }
2315
2316
                $translationWasFound = true;
2317
2318
                // If the translation is the version's only one, delete the version
2319
                if (count($versionInfo->languageCodes) < 2) {
2320
                    $this->persistenceHandler->contentHandler()->deleteVersion(
2321
                        $versionInfo->getContentInfo()->id,
2322
                        $versionInfo->versionNo
2323
                    );
2324
                }
2325
            }
2326
2327
            if (!$translationWasFound) {
2328
                throw new InvalidArgumentException(
2329
                    '$languageCode',
2330
                    sprintf(
2331
                        '%s does not exist in the Content item(id=%d)',
2332
                        $languageCode,
2333
                        $contentInfo->id
2334
                    )
2335
                );
2336
            }
2337
2338
            $this->persistenceHandler->contentHandler()->deleteTranslationFromContent(
2339
                $contentInfo->id,
2340
                $languageCode
2341
            );
2342
            $locationIds = array_map(
2343
                function (Location $location) {
2344
                    return $location->id;
2345
                },
2346
                $this->repository->getLocationService()->loadLocations($contentInfo)
2347
            );
2348
            $this->persistenceHandler->urlAliasHandler()->translationRemoved(
2349
                $locationIds,
2350
                $languageCode
2351
            );
2352
            $this->repository->commit();
2353
        } catch (InvalidArgumentException $e) {
2354
            $this->repository->rollback();
2355
            throw $e;
2356
        } catch (BadStateException $e) {
2357
            $this->repository->rollback();
2358
            throw $e;
2359
        } catch (UnauthorizedException $e) {
2360
            $this->repository->rollback();
2361
            throw $e;
2362
        } catch (Exception $e) {
2363
            $this->repository->rollback();
2364
            // cover generic unexpected exception to fulfill API promise on @throws
2365
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2366
        }
2367
    }
2368
2369
    /**
2370
     * Delete specified Translation from a Content Draft.
2371
     *
2372
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2373
     *         is the only one the Content Draft has or it is the main Translation of a Content Object.
2374
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2375
     *         to edit the Content (in one of the locations of the given Content Object).
2376
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2377
     *         is invalid for the given Draft.
2378
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if specified Version was not found
2379
     *
2380
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo Content Version Draft
2381
     * @param string $languageCode Language code of the Translation to be removed
2382
     *
2383
     * @return \eZ\Publish\API\Repository\Values\Content\Content Content Draft w/o the specified Translation
2384
     *
2385
     * @since 6.12
2386
     */
2387
    public function deleteTranslationFromDraft(APIVersionInfo $versionInfo, string $languageCode): APIContent
2388
    {
2389
        if (!$versionInfo->isDraft()) {
2390
            throw new BadStateException(
2391
                '$versionInfo',
2392
                'The version is not a draft, so translations cannot be modified. Create a draft before proceeding'
2393
            );
2394
        }
2395
2396
        if ($versionInfo->contentInfo->mainLanguageCode === $languageCode) {
2397
            throw new BadStateException(
2398
                '$languageCode',
2399
                'the specified translation is the main translation of the Content item. Change it before proceeding.'
2400
            );
2401
        }
2402
2403
        if (!$this->permissionResolver->canUser('content', 'edit', $versionInfo->contentInfo)) {
2404
            throw new UnauthorizedException(
2405
                'content', 'edit', ['contentId' => $versionInfo->contentInfo->id]
2406
            );
2407
        }
2408
2409
        if (!in_array($languageCode, $versionInfo->languageCodes)) {
2410
            throw new InvalidArgumentException(
2411
                '$languageCode',
2412
                sprintf(
2413
                    'The version (ContentId=%d, VersionNo=%d) is not translated into %s',
2414
                    $versionInfo->contentInfo->id,
2415
                    $versionInfo->versionNo,
2416
                    $languageCode
2417
                )
2418
            );
2419
        }
2420
2421
        if (count($versionInfo->languageCodes) === 1) {
2422
            throw new BadStateException(
2423
                '$languageCode',
2424
                'The provided translation is the only translation in this version'
2425
            );
2426
        }
2427
2428
        $this->repository->beginTransaction();
2429
        try {
2430
            $spiContent = $this->persistenceHandler->contentHandler()->deleteTranslationFromDraft(
2431
                $versionInfo->contentInfo->id,
2432
                $versionInfo->versionNo,
2433
                $languageCode
2434
            );
2435
            $this->repository->commit();
2436
2437
            return $this->contentDomainMapper->buildContentDomainObject(
2438
                $spiContent,
2439
                $this->repository->getContentTypeService()->loadContentType(
2440
                    $spiContent->versionInfo->contentInfo->contentTypeId
2441
                )
2442
            );
2443
        } catch (APINotFoundException $e) {
2444
            // avoid wrapping expected NotFoundException in BadStateException handled below
2445
            $this->repository->rollback();
2446
            throw $e;
2447
        } catch (Exception $e) {
2448
            $this->repository->rollback();
2449
            // cover generic unexpected exception to fulfill API promise on @throws
2450
            throw new BadStateException('$contentInfo', 'Could not remove the translation', $e);
2451
        }
2452
    }
2453
2454
    /**
2455
     * Hides Content by making all the Locations appear hidden.
2456
     * It does not persist hidden state on Location object itself.
2457
     *
2458
     * Content hidden by this API can be revealed by revealContent API.
2459
     *
2460
     * @see revealContent
2461
     *
2462
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2463
     */
2464
    public function hideContent(ContentInfo $contentInfo): void
2465
    {
2466
        if (!$this->permissionResolver->canUser('content', 'hide', $contentInfo)) {
2467
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2468
        }
2469
2470
        $this->repository->beginTransaction();
2471
        try {
2472
            $this->persistenceHandler->contentHandler()->updateMetadata(
2473
                $contentInfo->id,
2474
                new SPIMetadataUpdateStruct([
2475
                    'isHidden' => true,
2476
                ])
2477
            );
2478
            $locationHandler = $this->persistenceHandler->locationHandler();
2479
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2480
            foreach ($childLocations as $childLocation) {
2481
                $locationHandler->setInvisible($childLocation->id);
2482
            }
2483
            $this->repository->commit();
2484
        } catch (Exception $e) {
2485
            $this->repository->rollback();
2486
            throw $e;
2487
        }
2488
    }
2489
2490
    /**
2491
     * Reveals Content hidden by hideContent API.
2492
     * Locations which were hidden before hiding Content will remain hidden.
2493
     *
2494
     * @see hideContent
2495
     *
2496
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2497
     */
2498
    public function revealContent(ContentInfo $contentInfo): void
2499
    {
2500
        if (!$this->permissionResolver->canUser('content', 'hide', $contentInfo)) {
2501
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2502
        }
2503
2504
        $this->repository->beginTransaction();
2505
        try {
2506
            $this->persistenceHandler->contentHandler()->updateMetadata(
2507
                $contentInfo->id,
2508
                new SPIMetadataUpdateStruct([
2509
                    'isHidden' => false,
2510
                ])
2511
            );
2512
            $locationHandler = $this->persistenceHandler->locationHandler();
2513
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2514
            foreach ($childLocations as $childLocation) {
2515
                $locationHandler->setVisible($childLocation->id);
2516
            }
2517
            $this->repository->commit();
2518
        } catch (Exception $e) {
2519
            $this->repository->rollback();
2520
            throw $e;
2521
        }
2522
    }
2523
2524
    /**
2525
     * Instantiates a new content create struct object.
2526
     *
2527
     * alwaysAvailable is set to the ContentType's defaultAlwaysAvailable
2528
     *
2529
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
2530
     * @param string $mainLanguageCode
2531
     *
2532
     * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
2533
     */
2534
    public function newContentCreateStruct(ContentType $contentType, string $mainLanguageCode): APIContentCreateStruct
2535
    {
2536
        return new ContentCreateStruct(
2537
            [
2538
                'contentType' => $contentType,
2539
                'mainLanguageCode' => $mainLanguageCode,
2540
                'alwaysAvailable' => $contentType->defaultAlwaysAvailable,
2541
            ]
2542
        );
2543
    }
2544
2545
    /**
2546
     * Instantiates a new content meta data update struct.
2547
     *
2548
     * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
2549
     */
2550
    public function newContentMetadataUpdateStruct(): ContentMetadataUpdateStruct
2551
    {
2552
        return new ContentMetadataUpdateStruct();
2553
    }
2554
2555
    /**
2556
     * Instantiates a new content update struct.
2557
     *
2558
     * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
2559
     */
2560
    public function newContentUpdateStruct(): APIContentUpdateStruct
2561
    {
2562
        return new ContentUpdateStruct();
2563
    }
2564
2565
    /**
2566
     * @param \eZ\Publish\API\Repository\Values\User\User|null $user
2567
     *
2568
     * @return \eZ\Publish\API\Repository\Values\User\UserReference
2569
     */
2570
    private function resolveUser(?User $user): UserReference
2571
    {
2572
        if ($user === null) {
2573
            $user = $this->permissionResolver->getCurrentUserReference();
2574
        }
2575
2576
        return $user;
2577
    }
2578
}
2579