Completed
Push — content_service_typehint ( 668285...aaccb4 )
by
unknown
23:25 queued 08:48
created

ContentService::loadContentInfoByRemoteId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
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\Values\Content\Location;
23
use eZ\Publish\API\Repository\Values\Content\Language;
24
use eZ\Publish\SPI\Persistence\Handler;
25
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
26
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
27
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
28
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
29
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
30
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
31
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
32
use eZ\Publish\API\Repository\Values\User\User;
33
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
34
use eZ\Publish\API\Repository\Values\Content\Field;
35
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
36
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
37
use eZ\Publish\Core\Base\Exceptions\BadStateException;
38
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
39
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
40
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
41
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
42
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
43
use eZ\Publish\Core\FieldType\ValidationError;
44
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
45
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
46
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
47
use eZ\Publish\SPI\Limitation\Target;
48
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
49
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
50
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
51
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
52
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
53
use eZ\Publish\SPI\Persistence\Content\ContentInfo as SPIContentInfo;
54
use Exception;
55
56
/**
57
 * This class provides service methods for managing content.
58
 */
59
class ContentService implements ContentServiceInterface
60
{
61
    /** @var \eZ\Publish\Core\Repository\Repository */
62
    protected $repository;
63
64
    /** @var \eZ\Publish\SPI\Persistence\Handler */
65
    protected $persistenceHandler;
66
67
    /** @var array */
68
    protected $settings;
69
70
    /** @var \eZ\Publish\Core\Repository\Helper\DomainMapper */
71
    protected $domainMapper;
72
73
    /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
74
    protected $relationProcessor;
75
76
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
77
    protected $nameSchemaService;
78
79
    /** @var \eZ\Publish\Core\FieldType\FieldTypeRegistry */
80
    protected $fieldTypeRegistry;
81
82
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
83
    private $permissionResolver;
84
85
    public function __construct(
86
        RepositoryInterface $repository,
87
        Handler $handler,
88
        Helper\DomainMapper $domainMapper,
89
        Helper\RelationProcessor $relationProcessor,
90
        Helper\NameSchemaService $nameSchemaService,
91
        FieldTypeRegistry $fieldTypeRegistry,
92
        PermissionResolver $permissionResolver,
93
        array $settings = []
94
    ) {
95
        $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...
96
        $this->persistenceHandler = $handler;
97
        $this->domainMapper = $domainMapper;
98
        $this->relationProcessor = $relationProcessor;
99
        $this->nameSchemaService = $nameSchemaService;
100
        $this->fieldTypeRegistry = $fieldTypeRegistry;
101
        // Union makes sure default settings are ignored if provided in argument
102
        $this->settings = $settings + [
103
            // Version archive limit (0-50), only enforced on publish, not on un-publish.
104
            'default_version_archive_limit' => 5,
105
        ];
106
        $this->permissionResolver = $permissionResolver;
107
    }
108
109
    /**
110
     * Loads a content info object.
111
     *
112
     * To load fields use loadContent
113
     *
114
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
115
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
116
     *
117
     * @param int $contentId
118
     *
119
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
120
     */
121
    public function loadContentInfo(int $contentId): ContentInfo
122
    {
123
        $contentInfo = $this->internalLoadContentInfoById($contentId);
124
        if (!$this->permissionResolver->canUser('content', 'read', $contentInfo)) {
125
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
126
        }
127
128
        return $contentInfo;
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function loadContentInfoList(array $contentIds): iterable
135
    {
136
        $contentInfoList = [];
137
        $spiInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
138
        foreach ($spiInfoList as $id => $spiInfo) {
139
            $contentInfo = $this->domainMapper->buildContentInfoDomainObject($spiInfo);
140
            if ($this->permissionResolver->canUser('content', 'read', $contentInfo)) {
141
                $contentInfoList[$id] = $contentInfo;
142
            }
143
        }
144
145
        return $contentInfoList;
146
    }
147
148
    /**
149
     * Loads a content info object.
150
     *
151
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
152
     *
153
     * @param int $id
154
     *
155
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
156
     */
157
    public function internalLoadContentInfoById(int $id): ContentInfo
158
    {
159
        try {
160
            return $this->domainMapper->buildContentInfoDomainObject(
161
                $this->persistenceHandler->contentHandler()->loadContentInfo($id)
162
            );
163
        } catch (APINotFoundException $e) {
164
            throw new NotFoundException('Content', $id, $e);
165
        }
166
    }
167
168
    /**
169
     * Loads a content info object by remote id.
170
     *
171
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
172
     *
173
     * @param string $remoteId
174
     *
175
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
176
     */
177
    public function internalLoadContentInfoByRemoteId(string $remoteId): ContentInfo
178
    {
179
        try {
180
            return $this->domainMapper->buildContentInfoDomainObject(
181
                $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($remoteId)
182
            );
183
        } catch (APINotFoundException $e) {
184
            throw new NotFoundException('Content', $remoteId, $e);
185
        }
186
    }
187
188
    /**
189
     * Loads a content info object for the given remoteId.
190
     *
191
     * To load fields use loadContent
192
     *
193
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
194
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given remote id does not exist
195
     *
196
     * @param string $remoteId
197
     *
198
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
199
     */
200
    public function loadContentInfoByRemoteId(string $remoteId): ContentInfo
201
    {
202
        $contentInfo = $this->internalLoadContentInfoByRemoteId($remoteId);
203
204
        if (!$this->permissionResolver->canUser('content', 'read', $contentInfo)) {
205
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
206
        }
207
208
        return $contentInfo;
209
    }
210
211
    /**
212
     * Loads a version info of the given content object.
213
     *
214
     * If no version number is given, the method returns the current version
215
     *
216
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
217
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
218
     *
219
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
220
     * @param int|null $versionNo the version number. If not given the current version is returned.
221
     *
222
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
223
     */
224
    public function loadVersionInfo(ContentInfo $contentInfo, ?int $versionNo = null): APIVersionInfo
225
    {
226
        return $this->loadVersionInfoById($contentInfo->id, $versionNo);
227
    }
228
229
    /**
230
     * Loads a version info of the given content object id.
231
     *
232
     * If no version number is given, the method returns the current version
233
     *
234
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
235
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
236
     *
237
     * @param int $contentId
238
     * @param int|null $versionNo the version number. If not given the current version is returned.
239
     *
240
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
241
     */
242
    public function loadVersionInfoById(int $contentId, ?int $versionNo = null): APIVersionInfo
243
    {
244
        try {
245
            $spiVersionInfo = $this->persistenceHandler->contentHandler()->loadVersionInfo(
246
                $contentId,
247
                $versionNo
248
            );
249
        } catch (APINotFoundException $e) {
250
            throw new NotFoundException(
251
                'VersionInfo',
252
                [
253
                    'contentId' => $contentId,
254
                    'versionNo' => $versionNo,
255
                ],
256
                $e
257
            );
258
        }
259
260
        $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
261
262
        if ($versionInfo->isPublished()) {
263
            $function = 'read';
264
        } else {
265
            $function = 'versionread';
266
        }
267
268
        if (!$this->permissionResolver->canUser('content', $function, $versionInfo)) {
269
            throw new UnauthorizedException('content', $function, ['contentId' => $contentId]);
270
        }
271
272
        return $versionInfo;
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278
    public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages = null, ?int $versionNo = null, bool $useAlwaysAvailable = true): APIContent
279
    {
280
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
281
        if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
282
            $useAlwaysAvailable = false;
283
        }
284
285
        return $this->loadContent(
286
            $contentInfo->id,
287
            $languages,
288
            $versionNo,// On purpose pass as-is and not use $contentInfo, to make sure to return actual current version on null
289
            $useAlwaysAvailable
290
        );
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages = null, bool $useAlwaysAvailable = true): APIContent
297
    {
298
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
299
        if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
300
            $useAlwaysAvailable = false;
301
        }
302
303
        return $this->loadContent(
304
            $versionInfo->getContentInfo()->id,
305
            $languages,
306
            $versionInfo->versionNo,
307
            $useAlwaysAvailable
308
        );
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function loadContent(int $contentId, array $languages = null, ?int $versionNo = null, bool $useAlwaysAvailable = true): APIContent
315
    {
316
        $content = $this->internalLoadContentById($contentId, $languages, $versionNo, $useAlwaysAvailable);
317
318
        if (!$this->permissionResolver->canUser('content', 'read', $content)) {
319
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
320
        }
321
        if (
322
            !$content->getVersionInfo()->isPublished()
323
            && !$this->permissionResolver->canUser('content', 'versionread', $content)
324
        ) {
325
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId, 'versionNo' => $versionNo]);
326
        }
327
328
        return $content;
329
    }
330
331
    public function internalLoadContentById(
332
        int $id,
333
        ?array $languages = null,
334
        int $versionNo = null,
335
        bool $useAlwaysAvailable = true
336
    ): APIContent {
337
        try {
338
            $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
339
340
            return $this->internalLoadContentBySPIContentInfo(
341
                $spiContentInfo,
342
                $languages,
343
                $versionNo,
344
                $useAlwaysAvailable
345
            );
346
        } catch (APINotFoundException $e) {
347
            throw new NotFoundException(
348
                'Content',
349
                [
350
                    'id' => $id,
351
                    'languages' => $languages,
352
                    'versionNo' => $versionNo,
353
                ],
354
                $e
355
            );
356
        }
357
    }
358
359
    public function internalLoadContentByRemoteId(
360
        string $remoteId,
361
        array $languages = null,
362
        int $versionNo = null,
363
        bool $useAlwaysAvailable = true
364
    ): APIContent {
365
        try {
366
            $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($remoteId);
367
368
            return $this->internalLoadContentBySPIContentInfo(
369
                $spiContentInfo,
370
                $languages,
371
                $versionNo,
372
                $useAlwaysAvailable
373
            );
374
        } catch (APINotFoundException $e) {
375
            throw new NotFoundException(
376
                'Content',
377
                [
378
                    'remoteId' => $remoteId,
379
                    'languages' => $languages,
380
                    'versionNo' => $versionNo,
381
                ],
382
                $e
383
            );
384
        }
385
    }
386
387
    private function internalLoadContentBySPIContentInfo(SPIContentInfo $spiContentInfo, array $languages = null, int $versionNo = null, bool $useAlwaysAvailable = true): APIContent
388
    {
389
        $loadLanguages = $languages;
390
        $alwaysAvailableLanguageCode = null;
391
        // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
392
        // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
393
        if (!empty($loadLanguages) && $useAlwaysAvailable && $spiContentInfo->alwaysAvailable) {
394
            $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
395
            $loadLanguages = array_unique($loadLanguages);
396
        }
397
398
        $spiContent = $this->persistenceHandler->contentHandler()->load(
399
            $spiContentInfo->id,
400
            $versionNo,
401
            $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 389 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...
402
        );
403
404
        if ($languages === null) {
405
            $languages = [];
406
        }
407
408
        return $this->domainMapper->buildContentDomainObject(
409
            $spiContent,
410
            $this->repository->getContentTypeService()->loadContentType(
411
                $spiContent->versionInfo->contentInfo->contentTypeId,
412
                $languages
413
            ),
414
            $languages,
415
            $alwaysAvailableLanguageCode
416
        );
417
    }
418
419
    /**
420
     * Loads content in a version for the content object reference by the given remote id.
421
     *
422
     * If no version is given, the method returns the current version
423
     *
424
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content or version with the given remote id does not exist
425
     * @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
426
     *
427
     * @param string $remoteId
428
     * @param array $languages A language filter for fields. If not given all languages are returned
429
     * @param int $versionNo the version number. If not given the current version is returned
430
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
431
     *
432
     * @return \eZ\Publish\API\Repository\Values\Content\Content
433
     */
434
    public function loadContentByRemoteId(string $remoteId, array $languages = null, ?int $versionNo = null, bool $useAlwaysAvailable = true): APIContent
435
    {
436
        $content = $this->internalLoadContentByRemoteId($remoteId, $languages, $versionNo, $useAlwaysAvailable);
437
438
        if (!$this->permissionResolver->canUser('content', 'read', $content)) {
439
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
440
        }
441
442
        if (
443
            !$content->getVersionInfo()->isPublished()
444
            && !$this->permissionResolver->canUser('content', 'versionread', $content)
445
        ) {
446
            throw new UnauthorizedException('content', 'versionread', ['remoteId' => $remoteId, 'versionNo' => $versionNo]);
447
        }
448
449
        return $content;
450
    }
451
452
    /**
453
     * Bulk-load Content items by the list of ContentInfo Value Objects.
454
     *
455
     * Note: it does not throw exceptions on load, just ignores erroneous Content item.
456
     * Moreover, since the method works on pre-loaded ContentInfo list, it is assumed that user is
457
     * allowed to access every Content on the list.
458
     *
459
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo[] $contentInfoList
460
     * @param string[] $languages A language priority, filters returned fields and is used as prioritized language code on
461
     *                            returned value object. If not given all languages are returned.
462
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true,
463
     *                                 unless all languages have been asked for.
464
     *
465
     * @return \eZ\Publish\API\Repository\Values\Content\Content[] list of Content items with Content Ids as keys
466
     */
467
    public function loadContentListByContentInfo(
468
        array $contentInfoList,
469
        array $languages = [],
470
        bool $useAlwaysAvailable = true
471
    ): iterable {
472
        $loadAllLanguages = $languages === Language::ALL;
473
        $contentIds = [];
474
        $contentTypeIds = [];
475
        $translations = $languages;
476
        foreach ($contentInfoList as $contentInfo) {
477
            $contentIds[] = $contentInfo->id;
478
            $contentTypeIds[] = $contentInfo->contentTypeId;
479
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
480
            // Might in some case load more languages then intended, but prioritised handling will pick right one
481
            if (!$loadAllLanguages && $useAlwaysAvailable && $contentInfo->alwaysAvailable) {
482
                $translations[] = $contentInfo->mainLanguageCode;
483
            }
484
        }
485
486
        $contentList = [];
487
        $translations = array_unique($translations);
488
        $spiContentList = $this->persistenceHandler->contentHandler()->loadContentList(
489
            $contentIds,
490
            $translations
491
        );
492
        $contentTypeList = $this->repository->getContentTypeService()->loadContentTypeList(
493
            array_unique($contentTypeIds),
494
            $languages
495
        );
496
        foreach ($spiContentList as $contentId => $spiContent) {
497
            $contentInfo = $spiContent->versionInfo->contentInfo;
498
            $contentList[$contentId] = $this->domainMapper->buildContentDomainObject(
499
                $spiContent,
500
                $contentTypeList[$contentInfo->contentTypeId],
501
                $languages,
502
                $contentInfo->alwaysAvailable ? $contentInfo->mainLanguageCode : null
503
            );
504
        }
505
506
        return $contentList;
507
    }
508
509
    /**
510
     * Creates a new content draft assigned to the authenticated user.
511
     *
512
     * If a different userId is given in $contentCreateStruct it is assigned to the given user
513
     * but this required special rights for the authenticated user
514
     * (this is useful for content staging where the transfer process does not
515
     * have to authenticate with the user which created the content object in the source server).
516
     * The user has to publish the draft if it should be visible.
517
     * In 4.x at least one location has to be provided in the location creation array.
518
     *
519
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to create the content in the given location
520
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the provided remoteId exists in the system, required properties on
521
     *                                                                        struct are missing or invalid, or if multiple locations are under the
522
     *                                                                        same parent.
523
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
524
     *                                                                               or if a required field is missing / set to an empty value.
525
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
526
     *                                                                          or value is set for non-translatable field in language
527
     *                                                                          other than main.
528
     *
529
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
530
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs For each location parent under which a location should be created for the content
531
     *
532
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
533
     */
534
    public function createContent(APIContentCreateStruct $contentCreateStruct, array $locationCreateStructs = []): APIContent
535
    {
536
        if ($contentCreateStruct->mainLanguageCode === null) {
537
            throw new InvalidArgumentException('$contentCreateStruct', "'mainLanguageCode' property must be set");
538
        }
539
540
        if ($contentCreateStruct->contentType === null) {
541
            throw new InvalidArgumentException('$contentCreateStruct', "'contentType' property must be set");
542
        }
543
544
        $contentCreateStruct = clone $contentCreateStruct;
545
546
        if ($contentCreateStruct->ownerId === null) {
547
            $contentCreateStruct->ownerId = $this->permissionResolver->getCurrentUserReference()->getUserId();
548
        }
549
550
        if ($contentCreateStruct->alwaysAvailable === null) {
551
            $contentCreateStruct->alwaysAvailable = $contentCreateStruct->contentType->defaultAlwaysAvailable ?: false;
552
        }
553
554
        $contentCreateStruct->contentType = $this->repository->getContentTypeService()->loadContentType(
555
            $contentCreateStruct->contentType->id
556
        );
557
558
        if (empty($contentCreateStruct->sectionId)) {
559
            if (isset($locationCreateStructs[0])) {
560
                $location = $this->repository->getLocationService()->loadLocation(
561
                    $locationCreateStructs[0]->parentLocationId
562
                );
563
                $contentCreateStruct->sectionId = $location->contentInfo->sectionId;
564
            } else {
565
                $contentCreateStruct->sectionId = 1;
566
            }
567
        }
568
569
        if (!$this->permissionResolver->canUser('content', 'create', $contentCreateStruct, $locationCreateStructs)) {
570
            throw new UnauthorizedException(
571
                'content',
572
                'create',
573
                [
574
                    'parentLocationId' => isset($locationCreateStructs[0]) ?
575
                            $locationCreateStructs[0]->parentLocationId :
576
                            null,
577
                    'sectionId' => $contentCreateStruct->sectionId,
578
                ]
579
            );
580
        }
581
582
        if (!empty($contentCreateStruct->remoteId)) {
583
            try {
584
                $this->loadContentByRemoteId($contentCreateStruct->remoteId);
585
586
                throw new InvalidArgumentException(
587
                    '$contentCreateStruct',
588
                    "Another content with remoteId '{$contentCreateStruct->remoteId}' exists"
589
                );
590
            } catch (APINotFoundException $e) {
591
                // Do nothing
592
            }
593
        } else {
594
            $contentCreateStruct->remoteId = $this->domainMapper->getUniqueHash($contentCreateStruct);
595
        }
596
597
        $spiLocationCreateStructs = $this->buildSPILocationCreateStructs($locationCreateStructs);
598
599
        $languageCodes = $this->getLanguageCodesForCreate($contentCreateStruct);
600
        $fields = $this->mapFieldsForCreate($contentCreateStruct);
601
602
        $fieldValues = [];
603
        $spiFields = [];
604
        $allFieldErrors = [];
605
        $inputRelations = [];
606
        $locationIdToContentIdMapping = [];
607
608
        foreach ($contentCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
609
            /** @var $fieldType \eZ\Publish\Core\FieldType\FieldType */
610
            $fieldType = $this->fieldTypeRegistry->getFieldType(
611
                $fieldDefinition->fieldTypeIdentifier
612
            );
613
614
            foreach ($languageCodes as $languageCode) {
615
                $isEmptyValue = false;
616
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $contentCreateStruct->mainLanguageCode;
617
                $isLanguageMain = $languageCode === $contentCreateStruct->mainLanguageCode;
618
                if (isset($fields[$fieldDefinition->identifier][$valueLanguageCode])) {
619
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
620
                } else {
621
                    $fieldValue = $fieldDefinition->defaultValue;
622
                }
623
624
                $fieldValue = $fieldType->acceptValue($fieldValue);
625
626
                if ($fieldType->isEmptyValue($fieldValue)) {
627
                    $isEmptyValue = true;
628
                    if ($fieldDefinition->isRequired) {
629
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
630
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
631
                            null,
632
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
633
                            'empty'
634
                        );
635
                    }
636
                } else {
637
                    $fieldErrors = $fieldType->validate(
638
                        $fieldDefinition,
639
                        $fieldValue
640
                    );
641
                    if (!empty($fieldErrors)) {
642
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
643
                    }
644
                }
645
646
                if (!empty($allFieldErrors)) {
647
                    continue;
648
                }
649
650
                $this->relationProcessor->appendFieldRelations(
651
                    $inputRelations,
652
                    $locationIdToContentIdMapping,
653
                    $fieldType,
654
                    $fieldValue,
655
                    $fieldDefinition->id
656
                );
657
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
658
659
                // Only non-empty value for: translatable field or in main language
660
                if (
661
                    (!$isEmptyValue && $fieldDefinition->isTranslatable) ||
662
                    (!$isEmptyValue && $isLanguageMain)
663
                ) {
664
                    $spiFields[] = new SPIField(
665
                        [
666
                            'id' => null,
667
                            'fieldDefinitionId' => $fieldDefinition->id,
668
                            'type' => $fieldDefinition->fieldTypeIdentifier,
669
                            'value' => $fieldType->toPersistenceValue($fieldValue),
670
                            'languageCode' => $languageCode,
671
                            'versionNo' => null,
672
                        ]
673
                    );
674
                }
675
            }
676
        }
677
678
        if (!empty($allFieldErrors)) {
679
            throw new ContentFieldValidationException($allFieldErrors);
680
        }
681
682
        $spiContentCreateStruct = new SPIContentCreateStruct(
683
            [
684
                'name' => $this->nameSchemaService->resolve(
685
                    $contentCreateStruct->contentType->nameSchema,
686
                    $contentCreateStruct->contentType,
687
                    $fieldValues,
688
                    $languageCodes
689
                ),
690
                'typeId' => $contentCreateStruct->contentType->id,
691
                'sectionId' => $contentCreateStruct->sectionId,
692
                'ownerId' => $contentCreateStruct->ownerId,
693
                'locations' => $spiLocationCreateStructs,
694
                'fields' => $spiFields,
695
                'alwaysAvailable' => $contentCreateStruct->alwaysAvailable,
696
                'remoteId' => $contentCreateStruct->remoteId,
697
                'modified' => isset($contentCreateStruct->modificationDate) ? $contentCreateStruct->modificationDate->getTimestamp() : time(),
698
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
699
                    $contentCreateStruct->mainLanguageCode
700
                )->id,
701
            ]
702
        );
703
704
        $defaultObjectStates = $this->getDefaultObjectStates();
705
706
        $this->repository->beginTransaction();
707
        try {
708
            $spiContent = $this->persistenceHandler->contentHandler()->create($spiContentCreateStruct);
709
            $this->relationProcessor->processFieldRelations(
710
                $inputRelations,
711
                $spiContent->versionInfo->contentInfo->id,
712
                $spiContent->versionInfo->versionNo,
713
                $contentCreateStruct->contentType
714
            );
715
716
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
717
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
718
                $objectStateHandler->setContentState(
719
                    $spiContent->versionInfo->contentInfo->id,
720
                    $objectStateGroupId,
721
                    $objectState->id
722
                );
723
            }
724
725
            $this->repository->commit();
726
        } catch (Exception $e) {
727
            $this->repository->rollback();
728
            throw $e;
729
        }
730
731
        return $this->domainMapper->buildContentDomainObject(
732
            $spiContent,
733
            $contentCreateStruct->contentType
734
        );
735
    }
736
737
    /**
738
     * Returns an array of default content states with content state group id as key.
739
     *
740
     * @return \eZ\Publish\SPI\Persistence\Content\ObjectState[]
741
     */
742
    protected function getDefaultObjectStates(): array
743
    {
744
        $defaultObjectStatesMap = [];
745
        $objectStateHandler = $this->persistenceHandler->objectStateHandler();
746
747
        foreach ($objectStateHandler->loadAllGroups() as $objectStateGroup) {
748
            foreach ($objectStateHandler->loadObjectStates($objectStateGroup->id) as $objectState) {
749
                // Only register the first object state which is the default one.
750
                $defaultObjectStatesMap[$objectStateGroup->id] = $objectState;
751
                break;
752
            }
753
        }
754
755
        return $defaultObjectStatesMap;
756
    }
757
758
    /**
759
     * Returns all language codes used in given $fields.
760
     *
761
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value is set in main language
762
     *
763
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
764
     *
765
     * @return string[]
766
     */
767
    protected function getLanguageCodesForCreate(APIContentCreateStruct $contentCreateStruct): array
768
    {
769
        $languageCodes = [];
770
771
        foreach ($contentCreateStruct->fields as $field) {
772
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
773
                continue;
774
            }
775
776
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
777
                $field->languageCode
778
            );
779
            $languageCodes[$field->languageCode] = true;
780
        }
781
782
        if (!isset($languageCodes[$contentCreateStruct->mainLanguageCode])) {
783
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
784
                $contentCreateStruct->mainLanguageCode
785
            );
786
            $languageCodes[$contentCreateStruct->mainLanguageCode] = true;
787
        }
788
789
        return array_keys($languageCodes);
790
    }
791
792
    /**
793
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
794
     *
795
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
796
     *                                                                          or value is set for non-translatable field in language
797
     *                                                                          other than main
798
     *
799
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
800
     *
801
     * @return array
802
     */
803
    protected function mapFieldsForCreate(APIContentCreateStruct $contentCreateStruct): array
804
    {
805
        $fields = [];
806
807
        foreach ($contentCreateStruct->fields as $field) {
808
            $fieldDefinition = $contentCreateStruct->contentType->getFieldDefinition($field->fieldDefIdentifier);
809
810
            if ($fieldDefinition === null) {
811
                throw new ContentValidationException(
812
                    "Field definition '%identifier%' does not exist in given ContentType",
813
                    ['%identifier%' => $field->fieldDefIdentifier]
814
                );
815
            }
816
817
            if ($field->languageCode === null) {
818
                $field = $this->cloneField(
819
                    $field,
820
                    ['languageCode' => $contentCreateStruct->mainLanguageCode]
821
                );
822
            }
823
824
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $contentCreateStruct->mainLanguageCode)) {
825
                throw new ContentValidationException(
826
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
827
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
828
                );
829
            }
830
831
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
832
        }
833
834
        return $fields;
835
    }
836
837
    /**
838
     * Clones $field with overriding specific properties from given $overrides array.
839
     *
840
     * @param Field $field
841
     * @param array $overrides
842
     *
843
     * @return Field
844
     */
845
    private function cloneField(Field $field, array $overrides = []): Field
846
    {
847
        $fieldData = array_merge(
848
            [
849
                'id' => $field->id,
850
                'value' => $field->value,
851
                'languageCode' => $field->languageCode,
852
                'fieldDefIdentifier' => $field->fieldDefIdentifier,
853
                'fieldTypeIdentifier' => $field->fieldTypeIdentifier,
854
            ],
855
            $overrides
856
        );
857
858
        return new Field($fieldData);
859
    }
860
861
    /**
862
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
863
     *
864
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs
865
     *
866
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct[]
867
     */
868
    protected function buildSPILocationCreateStructs(array $locationCreateStructs): array
869
    {
870
        $spiLocationCreateStructs = [];
871
        $parentLocationIdSet = [];
872
        $mainLocation = true;
873
874
        foreach ($locationCreateStructs as $locationCreateStruct) {
875
            if (isset($parentLocationIdSet[$locationCreateStruct->parentLocationId])) {
876
                throw new InvalidArgumentException(
877
                    '$locationCreateStructs',
878
                    "Multiple LocationCreateStructs with the same parent Location '{$locationCreateStruct->parentLocationId}' are given"
879
                );
880
            }
881
882
            if (!array_key_exists($locationCreateStruct->sortField, Location::SORT_FIELD_MAP)) {
883
                $locationCreateStruct->sortField = Location::SORT_FIELD_NAME;
884
            }
885
886
            if (!array_key_exists($locationCreateStruct->sortOrder, Location::SORT_ORDER_MAP)) {
887
                $locationCreateStruct->sortOrder = Location::SORT_ORDER_ASC;
888
            }
889
890
            $parentLocationIdSet[$locationCreateStruct->parentLocationId] = true;
891
            $parentLocation = $this->repository->getLocationService()->loadLocation(
892
                $locationCreateStruct->parentLocationId
893
            );
894
895
            $spiLocationCreateStructs[] = $this->domainMapper->buildSPILocationCreateStruct(
896
                $locationCreateStruct,
897
                $parentLocation,
898
                $mainLocation,
899
                // For Content draft contentId and contentVersionNo are set in ContentHandler upon draft creation
900
                null,
901
                null
902
            );
903
904
            // First Location in the list will be created as main Location
905
            $mainLocation = false;
906
        }
907
908
        return $spiLocationCreateStructs;
909
    }
910
911
    /**
912
     * Updates the metadata.
913
     *
914
     * (see {@link ContentMetadataUpdateStruct}) of a content object - to update fields use updateContent
915
     *
916
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update the content meta data
917
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the remoteId in $contentMetadataUpdateStruct is set but already exists
918
     *
919
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
920
     * @param \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct $contentMetadataUpdateStruct
921
     *
922
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content with the updated attributes
923
     */
924
    public function updateContentMetadata(ContentInfo $contentInfo, ContentMetadataUpdateStruct $contentMetadataUpdateStruct): APIContent
925
    {
926
        $propertyCount = 0;
927
        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...
928
            if (isset($contentMetadataUpdateStruct->$propertyName)) {
929
                $propertyCount += 1;
930
            }
931
        }
932
        if ($propertyCount === 0) {
933
            throw new InvalidArgumentException(
934
                '$contentMetadataUpdateStruct',
935
                'At least one property must be set'
936
            );
937
        }
938
939
        $loadedContentInfo = $this->loadContentInfo($contentInfo->id);
940
941
        if (!$this->permissionResolver->canUser('content', 'edit', $loadedContentInfo)) {
942
            throw new UnauthorizedException('content', 'edit', ['contentId' => $loadedContentInfo->id]);
943
        }
944
945
        if (isset($contentMetadataUpdateStruct->remoteId)) {
946
            try {
947
                $existingContentInfo = $this->loadContentInfoByRemoteId($contentMetadataUpdateStruct->remoteId);
948
949
                if ($existingContentInfo->id !== $loadedContentInfo->id) {
950
                    throw new InvalidArgumentException(
951
                        '$contentMetadataUpdateStruct',
952
                        "Another content with remoteId '{$contentMetadataUpdateStruct->remoteId}' exists"
953
                    );
954
                }
955
            } catch (APINotFoundException $e) {
956
                // Do nothing
957
            }
958
        }
959
960
        $this->repository->beginTransaction();
961
        try {
962
            if ($propertyCount > 1 || !isset($contentMetadataUpdateStruct->mainLocationId)) {
963
                $this->persistenceHandler->contentHandler()->updateMetadata(
964
                    $loadedContentInfo->id,
965
                    new SPIMetadataUpdateStruct(
966
                        [
967
                            'ownerId' => $contentMetadataUpdateStruct->ownerId,
968
                            'publicationDate' => isset($contentMetadataUpdateStruct->publishedDate) ?
969
                                $contentMetadataUpdateStruct->publishedDate->getTimestamp() :
970
                                null,
971
                            'modificationDate' => isset($contentMetadataUpdateStruct->modificationDate) ?
972
                                $contentMetadataUpdateStruct->modificationDate->getTimestamp() :
973
                                null,
974
                            'mainLanguageId' => isset($contentMetadataUpdateStruct->mainLanguageCode) ?
975
                                $this->repository->getContentLanguageService()->loadLanguage(
976
                                    $contentMetadataUpdateStruct->mainLanguageCode
977
                                )->id :
978
                                null,
979
                            'alwaysAvailable' => $contentMetadataUpdateStruct->alwaysAvailable,
980
                            'remoteId' => $contentMetadataUpdateStruct->remoteId,
981
                            'name' => $contentMetadataUpdateStruct->name,
982
                        ]
983
                    )
984
                );
985
            }
986
987
            // Change main location
988
            if (isset($contentMetadataUpdateStruct->mainLocationId)
989
                && $loadedContentInfo->mainLocationId !== $contentMetadataUpdateStruct->mainLocationId) {
990
                $this->persistenceHandler->locationHandler()->changeMainLocation(
991
                    $loadedContentInfo->id,
992
                    $contentMetadataUpdateStruct->mainLocationId
993
                );
994
            }
995
996
            // Republish URL aliases to update always-available flag
997
            if (isset($contentMetadataUpdateStruct->alwaysAvailable)
998
                && $loadedContentInfo->alwaysAvailable !== $contentMetadataUpdateStruct->alwaysAvailable) {
999
                $content = $this->loadContent($loadedContentInfo->id);
1000
                $this->publishUrlAliasesForContent($content, false);
1001
            }
1002
1003
            $this->repository->commit();
1004
        } catch (Exception $e) {
1005
            $this->repository->rollback();
1006
            throw $e;
1007
        }
1008
1009
        return isset($content) ? $content : $this->loadContent($loadedContentInfo->id);
1010
    }
1011
1012
    /**
1013
     * Publishes URL aliases for all locations of a given content.
1014
     *
1015
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1016
     * @param bool $updatePathIdentificationString this parameter is legacy storage specific for updating
1017
     *                      ezcontentobject_tree.path_identification_string, it is ignored by other storage engines
1018
     */
1019
    protected function publishUrlAliasesForContent(APIContent $content, bool $updatePathIdentificationString = true): void
1020
    {
1021
        $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
1022
        $locations = $this->repository->getLocationService()->loadLocations(
1023
            $content->getVersionInfo()->getContentInfo()
1024
        );
1025
        $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
1026
        foreach ($locations as $location) {
1027
            foreach ($urlAliasNames as $languageCode => $name) {
1028
                $urlAliasHandler->publishUrlAliasForLocation(
1029
                    $location->id,
1030
                    $location->parentLocationId,
1031
                    $name,
1032
                    $languageCode,
1033
                    $content->contentInfo->alwaysAvailable,
1034
                    $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...
1035
                );
1036
            }
1037
            // archive URL aliases of Translations that got deleted
1038
            $urlAliasHandler->archiveUrlAliasesForDeletedTranslations(
1039
                $location->id,
1040
                $location->parentLocationId,
1041
                $content->versionInfo->languageCodes
1042
            );
1043
        }
1044
    }
1045
1046
    /**
1047
     * Deletes a content object including all its versions and locations including their subtrees.
1048
     *
1049
     * @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)
1050
     *
1051
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1052
     *
1053
     * @return mixed[] Affected Location Id's
1054
     */
1055
    public function deleteContent(ContentInfo $contentInfo): iterable
1056
    {
1057
        $contentInfo = $this->internalLoadContentInfoById($contentInfo->id);
1058
1059
        if (!$this->permissionResolver->canUser('content', 'remove', $contentInfo)) {
1060
            throw new UnauthorizedException('content', 'remove', ['contentId' => $contentInfo->id]);
1061
        }
1062
1063
        $affectedLocations = [];
1064
        $this->repository->beginTransaction();
1065
        try {
1066
            // Load Locations first as deleting Content also deletes belonging Locations
1067
            $spiLocations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentInfo->id);
1068
            $this->persistenceHandler->contentHandler()->deleteContent($contentInfo->id);
1069
            $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
1070
            foreach ($spiLocations as $spiLocation) {
1071
                $urlAliasHandler->locationDeleted($spiLocation->id);
1072
                $affectedLocations[] = $spiLocation->id;
1073
            }
1074
            $this->repository->commit();
1075
        } catch (Exception $e) {
1076
            $this->repository->rollback();
1077
            throw $e;
1078
        }
1079
1080
        return $affectedLocations;
1081
    }
1082
1083
    /**
1084
     * Creates a draft from a published or archived version.
1085
     *
1086
     * If no version is given, the current published version is used.
1087
     *
1088
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1089
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo|null $versionInfo
1090
     * @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
1091
     * @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.
1092
     *
1093
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
1094
     *
1095
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
1096
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the current-user is not allowed to create the draft
1097
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to create the draft
1098
     */
1099
    public function createContentDraft(
1100
        ContentInfo $contentInfo,
1101
        ?APIVersionInfo $versionInfo = null,
1102
        ?User $creator = null,
1103
        ?Language $language = null
1104
    ): APIContent {
1105
        $contentInfo = $this->loadContentInfo($contentInfo->id);
1106
1107
        if ($versionInfo !== null) {
1108
            // Check that given $contentInfo and $versionInfo belong to the same content
1109
            if ($versionInfo->getContentInfo()->id != $contentInfo->id) {
1110
                throw new InvalidArgumentException(
1111
                    '$versionInfo',
1112
                    'VersionInfo does not belong to the same content as given ContentInfo'
1113
                );
1114
            }
1115
1116
            $versionInfo = $this->loadVersionInfoById($contentInfo->id, $versionInfo->versionNo);
1117
1118
            switch ($versionInfo->status) {
1119
                case VersionInfo::STATUS_PUBLISHED:
1120
                case VersionInfo::STATUS_ARCHIVED:
1121
                    break;
1122
1123
                default:
1124
                    // @todo: throw an exception here, to be defined
1125
                    throw new BadStateException(
1126
                        '$versionInfo',
1127
                        'Draft can not be created from a draft version'
1128
                    );
1129
            }
1130
1131
            $versionNo = $versionInfo->versionNo;
1132
        } elseif ($contentInfo->published) {
1133
            $versionNo = $contentInfo->currentVersionNo;
1134
        } else {
1135
            // @todo: throw an exception here, to be defined
1136
            throw new BadStateException(
1137
                '$contentInfo',
1138
                'Content is not published, draft can be created only from published or archived version'
1139
            );
1140
        }
1141
1142
        if ($creator === null) {
1143
            $creator = $this->permissionResolver->getCurrentUserReference();
1144
        }
1145
1146
        $fallbackLanguageCode = $versionInfo->initialLanguageCode ?? $contentInfo->mainLanguageCode;
1147
        $languageCode = $language->languageCode ?? $fallbackLanguageCode;
1148
1149
        if (!$this->permissionResolver->canUser(
1150
            'content',
1151
            'edit',
1152
            $contentInfo,
1153
            [
1154
                (new Target\Builder\VersionBuilder())
1155
                    ->changeStatusTo(APIVersionInfo::STATUS_DRAFT)
1156
                    ->build(),
1157
            ]
1158
        )) {
1159
            throw new UnauthorizedException(
1160
                'content',
1161
                'edit',
1162
                ['contentId' => $contentInfo->id]
1163
            );
1164
        }
1165
1166
        $this->repository->beginTransaction();
1167
        try {
1168
            $spiContent = $this->persistenceHandler->contentHandler()->createDraftFromVersion(
1169
                $contentInfo->id,
1170
                $versionNo,
1171
                $creator->getUserId(),
1172
                $languageCode
1173
            );
1174
            $this->repository->commit();
1175
        } catch (Exception $e) {
1176
            $this->repository->rollback();
1177
            throw $e;
1178
        }
1179
1180
        return $this->domainMapper->buildContentDomainObject(
1181
            $spiContent,
1182
            $this->repository->getContentTypeService()->loadContentType(
1183
                $spiContent->versionInfo->contentInfo->contentTypeId
1184
            )
1185
        );
1186
    }
1187
1188
    public function countContentDrafts(?User $user = null): int
1189
    {
1190
        if ($this->permissionResolver->hasAccess('content', 'versionread') === false) {
1191
            return 0;
1192
        }
1193
1194
        return $this->persistenceHandler->contentHandler()->countDraftsForUser(
1195
            $this->resolveUser($user)->getUserId()
1196
        );
1197
    }
1198
1199
    /**
1200
     * Loads drafts for a user.
1201
     *
1202
     * If no user is given the drafts for the authenticated user are returned
1203
     *
1204
     * @param \eZ\Publish\API\Repository\Values\User\User|null $user
1205
     *
1206
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Drafts owned by the given user
1207
     *
1208
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1209
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1210
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1211
     */
1212
    public function loadContentDrafts(?User $user = null): iterable
1213
    {
1214
        // throw early if user has absolutely no access to versionread
1215
        if ($this->permissionResolver->hasAccess('content', 'versionread') === false) {
1216
            throw new UnauthorizedException('content', 'versionread');
1217
        }
1218
1219
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftsForUser(
1220
            $this->resolveUser($user)->getUserId()
1221
        );
1222
        $versionInfoList = [];
1223
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1224
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1225
            // @todo: Change this to filter returned drafts by permissions instead of throwing
1226
            if (!$this->permissionResolver->canUser('content', 'versionread', $versionInfo)) {
1227
                throw new UnauthorizedException('content', 'versionread', ['contentId' => $versionInfo->contentInfo->id]);
1228
            }
1229
1230
            $versionInfoList[] = $versionInfo;
1231
        }
1232
1233
        return $versionInfoList;
1234
    }
1235
1236
    public function loadContentDraftList(?User $user = null, int $offset = 0, int $limit = -1): ContentDraftList
1237
    {
1238
        $list = new ContentDraftList();
1239
        if ($this->permissionResolver->hasAccess('content', 'versionread') === false) {
1240
            return $list;
1241
        }
1242
1243
        $list->totalCount = $this->persistenceHandler->contentHandler()->countDraftsForUser(
1244
            $this->resolveUser($user)->getUserId()
1245
        );
1246
        if ($list->totalCount > 0) {
1247
            $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftListForUser(
1248
                $this->resolveUser($user)->getUserId(),
1249
                $offset,
1250
                $limit
1251
            );
1252
            foreach ($spiVersionInfoList as $spiVersionInfo) {
1253
                $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1254
                if ($this->permissionResolver->canUser('content', 'versionread', $versionInfo)) {
1255
                    $list->items[] = new ContentDraftListItem($versionInfo);
1256
                } else {
1257
                    $list->items[] = new UnauthorizedContentDraftListItem(
1258
                        'content',
1259
                        'versionread',
1260
                        ['contentId' => $versionInfo->contentInfo->id]
1261
                    );
1262
                }
1263
            }
1264
        }
1265
1266
        return $list;
1267
    }
1268
1269
    /**
1270
     * Updates the fields of a draft.
1271
     *
1272
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1273
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1274
     *
1275
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the updated fields
1276
     *
1277
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
1278
     *                                                                               or if a required field is missing / set to an empty value.
1279
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
1280
     *                                                                          or value is set for non-translatable field in language
1281
     *                                                                          other than main.
1282
     *
1283
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
1284
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1285
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
1286
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1287
     */
1288
    public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct): APIContent
1289
    {
1290
        $contentUpdateStruct = clone $contentUpdateStruct;
1291
1292
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1293
        $content = $this->loadContent(
1294
            $versionInfo->getContentInfo()->id,
1295
            null,
1296
            $versionInfo->versionNo
1297
        );
1298
        if (!$content->versionInfo->isDraft()) {
1299
            throw new BadStateException(
1300
                '$versionInfo',
1301
                'Version is not a draft and can not be updated'
1302
            );
1303
        }
1304
1305
        if (!$this->repository->getPermissionResolver()->canUser(
1306
            'content',
1307
            'edit',
1308
            $content,
1309
            [
1310
                (new Target\Builder\VersionBuilder())
1311
                    ->updateFieldsTo(
1312
                        $contentUpdateStruct->initialLanguageCode,
1313
                        $contentUpdateStruct->fields
1314
                    )
1315
                    ->build(),
1316
            ]
1317
        )) {
1318
            throw new UnauthorizedException('content', 'edit', ['contentId' => $content->id]);
1319
        }
1320
1321
        $mainLanguageCode = $content->contentInfo->mainLanguageCode;
1322
        if ($contentUpdateStruct->initialLanguageCode === null) {
1323
            $contentUpdateStruct->initialLanguageCode = $mainLanguageCode;
1324
        }
1325
1326
        $allLanguageCodes = $this->getLanguageCodesForUpdate($contentUpdateStruct, $content);
1327
        $contentLanguageHandler = $this->persistenceHandler->contentLanguageHandler();
1328
        foreach ($allLanguageCodes as $languageCode) {
1329
            $contentLanguageHandler->loadByLanguageCode($languageCode);
1330
        }
1331
1332
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1333
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1334
            $content->contentInfo->contentTypeId
1335
        );
1336
        $fields = $this->mapFieldsForUpdate(
1337
            $contentUpdateStruct,
1338
            $contentType,
1339
            $mainLanguageCode
1340
        );
1341
1342
        $fieldValues = [];
1343
        $spiFields = [];
1344
        $allFieldErrors = [];
1345
        $inputRelations = [];
1346
        $locationIdToContentIdMapping = [];
1347
1348
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1349
            /** @var $fieldType \eZ\Publish\SPI\FieldType\FieldType */
1350
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1351
                $fieldDefinition->fieldTypeIdentifier
1352
            );
1353
1354
            foreach ($allLanguageCodes as $languageCode) {
1355
                $isCopied = $isEmpty = $isRetained = false;
1356
                $isLanguageNew = !in_array($languageCode, $content->versionInfo->languageCodes);
1357
                $isLanguageUpdated = in_array($languageCode, $updatedLanguageCodes);
1358
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $mainLanguageCode;
1359
                $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
1360
                $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
1361
1362
                if (!$isFieldUpdated && !$isLanguageNew) {
1363
                    $isRetained = true;
1364
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1365
                } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
1366
                    $isCopied = true;
1367
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1368
                } elseif ($isFieldUpdated) {
1369
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
1370
                } else {
1371
                    $fieldValue = $fieldDefinition->defaultValue;
1372
                }
1373
1374
                $fieldValue = $fieldType->acceptValue($fieldValue);
1375
1376
                if ($fieldType->isEmptyValue($fieldValue)) {
1377
                    $isEmpty = true;
1378
                    if ($isLanguageUpdated && $fieldDefinition->isRequired) {
1379
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
1380
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
1381
                            null,
1382
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
1383
                            'empty'
1384
                        );
1385
                    }
1386
                } elseif ($isLanguageUpdated) {
1387
                    $fieldErrors = $fieldType->validate(
1388
                        $fieldDefinition,
1389
                        $fieldValue
1390
                    );
1391
                    if (!empty($fieldErrors)) {
1392
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
1393
                    }
1394
                }
1395
1396
                if (!empty($allFieldErrors)) {
1397
                    continue;
1398
                }
1399
1400
                $this->relationProcessor->appendFieldRelations(
1401
                    $inputRelations,
1402
                    $locationIdToContentIdMapping,
1403
                    $fieldType,
1404
                    $fieldValue,
1405
                    $fieldDefinition->id
1406
                );
1407
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
1408
1409
                if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
1410
                    continue;
1411
                }
1412
1413
                $spiFields[] = new SPIField(
1414
                    [
1415
                        'id' => $isLanguageNew ?
1416
                            null :
1417
                            $content->getField($fieldDefinition->identifier, $languageCode)->id,
1418
                        'fieldDefinitionId' => $fieldDefinition->id,
1419
                        'type' => $fieldDefinition->fieldTypeIdentifier,
1420
                        'value' => $fieldType->toPersistenceValue($fieldValue),
1421
                        'languageCode' => $languageCode,
1422
                        'versionNo' => $versionInfo->versionNo,
1423
                    ]
1424
                );
1425
            }
1426
        }
1427
1428
        if (!empty($allFieldErrors)) {
1429
            throw new ContentFieldValidationException($allFieldErrors);
1430
        }
1431
1432
        $spiContentUpdateStruct = new SPIContentUpdateStruct(
1433
            [
1434
                'name' => $this->nameSchemaService->resolveNameSchema(
1435
                    $content,
1436
                    $fieldValues,
1437
                    $allLanguageCodes,
1438
                    $contentType
1439
                ),
1440
                'creatorId' => $contentUpdateStruct->creatorId ?: $this->permissionResolver->getCurrentUserReference()->getUserId(),
1441
                'fields' => $spiFields,
1442
                'modificationDate' => time(),
1443
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1444
                    $contentUpdateStruct->initialLanguageCode
1445
                )->id,
1446
            ]
1447
        );
1448
        $existingRelations = $this->loadRelations($versionInfo);
1449
1450
        $this->repository->beginTransaction();
1451
        try {
1452
            $spiContent = $this->persistenceHandler->contentHandler()->updateContent(
1453
                $versionInfo->getContentInfo()->id,
1454
                $versionInfo->versionNo,
1455
                $spiContentUpdateStruct
1456
            );
1457
            $this->relationProcessor->processFieldRelations(
1458
                $inputRelations,
1459
                $spiContent->versionInfo->contentInfo->id,
1460
                $spiContent->versionInfo->versionNo,
1461
                $contentType,
1462
                $existingRelations
1463
            );
1464
            $this->repository->commit();
1465
        } catch (Exception $e) {
1466
            $this->repository->rollback();
1467
            throw $e;
1468
        }
1469
1470
        return $this->domainMapper->buildContentDomainObject(
1471
            $spiContent,
1472
            $contentType
1473
        );
1474
    }
1475
1476
    /**
1477
     * Returns only updated language codes.
1478
     *
1479
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1480
     *
1481
     * @return array
1482
     */
1483
    private function getUpdatedLanguageCodes(APIContentUpdateStruct $contentUpdateStruct): array
1484
    {
1485
        $languageCodes = [
1486
            $contentUpdateStruct->initialLanguageCode => true,
1487
        ];
1488
1489
        foreach ($contentUpdateStruct->fields as $field) {
1490
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
1491
                continue;
1492
            }
1493
1494
            $languageCodes[$field->languageCode] = true;
1495
        }
1496
1497
        return array_keys($languageCodes);
1498
    }
1499
1500
    /**
1501
     * Returns all language codes used in given $fields.
1502
     *
1503
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value exists in initial language
1504
     *
1505
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1506
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1507
     *
1508
     * @return array
1509
     */
1510
    protected function getLanguageCodesForUpdate(APIContentUpdateStruct $contentUpdateStruct, APIContent $content): array
1511
    {
1512
        $languageCodes = array_fill_keys($content->versionInfo->languageCodes, true);
1513
        $languageCodes[$contentUpdateStruct->initialLanguageCode] = true;
1514
1515
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1516
        foreach ($updatedLanguageCodes as $languageCode) {
1517
            $languageCodes[$languageCode] = true;
1518
        }
1519
1520
        return array_keys($languageCodes);
1521
    }
1522
1523
    /**
1524
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
1525
     *
1526
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
1527
     *                                                                          or value is set for non-translatable field in language
1528
     *                                                                          other than main
1529
     *
1530
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1531
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1532
     * @param string $mainLanguageCode
1533
     *
1534
     * @return array
1535
     */
1536
    protected function mapFieldsForUpdate(
1537
        APIContentUpdateStruct $contentUpdateStruct,
1538
        ContentType $contentType,
1539
        string $mainLanguageCode
1540
    ): array {
1541
        $fields = [];
1542
1543
        foreach ($contentUpdateStruct->fields as $field) {
1544
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1545
1546
            if ($fieldDefinition === null) {
1547
                throw new ContentValidationException(
1548
                    "Field definition '%identifier%' does not exist in given ContentType",
1549
                    ['%identifier%' => $field->fieldDefIdentifier]
1550
                );
1551
            }
1552
1553
            if ($field->languageCode === null) {
1554
                if ($fieldDefinition->isTranslatable) {
1555
                    $languageCode = $contentUpdateStruct->initialLanguageCode;
1556
                } else {
1557
                    $languageCode = $mainLanguageCode;
1558
                }
1559
                $field = $this->cloneField($field, ['languageCode' => $languageCode]);
1560
            }
1561
1562
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $mainLanguageCode)) {
1563
                throw new ContentValidationException(
1564
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
1565
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
1566
                );
1567
            }
1568
1569
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
1570
        }
1571
1572
        return $fields;
1573
    }
1574
1575
    /**
1576
     * Publishes a content version.
1577
     *
1578
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1579
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1580
     *
1581
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1582
     * @param string[] $translations
1583
     *
1584
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1585
     *
1586
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1587
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1588
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1589
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1590
     */
1591
    public function publishVersion(APIVersionInfo $versionInfo, array $translations = Language::ALL): APIContent
1592
    {
1593
        $content = $this->internalLoadContentById(
1594
            $versionInfo->contentInfo->id,
1595
            null,
1596
            $versionInfo->versionNo
1597
        );
1598
1599
        $fromContent = null;
0 ignored issues
show
Unused Code introduced by
$fromContent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1600
        if ($content->contentInfo->currentVersionNo !== $versionInfo->versionNo) {
1601
            $fromContent = $this->internalLoadContentById(
1602
                $content->contentInfo->id,
1603
                null,
1604
                $content->contentInfo->currentVersionNo
1605
            );
1606
            // should not occur now, might occur in case of un-publish
1607
            if (!$fromContent->contentInfo->isPublished()) {
1608
                $fromContent = null;
0 ignored issues
show
Unused Code introduced by
$fromContent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1609
            }
1610
        }
1611
1612
        if (!$this->permissionResolver->canUser(
1613
            'content',
1614
            'publish',
1615
            $content
1616
        )) {
1617
            throw new UnauthorizedException(
1618
                'content', 'publish', ['contentId' => $content->id]
1619
            );
1620
        }
1621
1622
        $this->repository->beginTransaction();
1623
        try {
1624
            $this->copyTranslationsFromPublishedVersion($content->versionInfo, $translations);
1625
            $content = $this->internalPublishVersion($content->getVersionInfo(), null);
1626
            $this->repository->commit();
1627
        } catch (Exception $e) {
1628
            $this->repository->rollback();
1629
            throw $e;
1630
        }
1631
1632
        return $content;
1633
    }
1634
1635
    /**
1636
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1637
     * @param array $translations
1638
     *
1639
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1640
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException
1641
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
1642
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1643
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1644
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1645
     */
1646
    protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionInfo, array $translations = []): void
1647
    {
1648
        $contendId = $versionInfo->contentInfo->id;
1649
1650
        $currentContent = $this->internalLoadContentById($contendId);
1651
        $currentVersionInfo = $currentContent->versionInfo;
1652
1653
        // Copying occurs only if:
1654
        // - There is published Version
1655
        // - Published version is older than the currently published one unless specific translations are provided.
1656
        if (!$currentVersionInfo->isPublished() ||
1657
            ($versionInfo->versionNo >= $currentVersionInfo->versionNo && empty($translations))) {
1658
            return;
1659
        }
1660
1661
        if (empty($translations)) {
1662
            $languagesToCopy = array_diff(
1663
                $currentVersionInfo->languageCodes,
1664
                $versionInfo->languageCodes
1665
            );
1666
        } else {
1667
            $languagesToCopy = array_diff(
1668
                $currentVersionInfo->languageCodes,
1669
                $translations
1670
            );
1671
        }
1672
1673
        if (empty($languagesToCopy)) {
1674
            return;
1675
        }
1676
1677
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1678
            $currentVersionInfo->contentInfo->contentTypeId
1679
        );
1680
1681
        // Find only translatable fields to update with selected languages
1682
        $updateStruct = $this->newContentUpdateStruct();
1683
        $updateStruct->initialLanguageCode = $versionInfo->initialLanguageCode;
1684
1685
        $contentToPublish = $this->internalLoadContentById($contendId, null, $versionInfo->versionNo);
1686
        $fallbackUpdateStruct = $this->newContentUpdateStruct();
1687
1688
        foreach ($currentContent->getFields() as $field) {
1689
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1690
1691
            if (!$fieldDefinition->isTranslatable || !\in_array($field->languageCode, $languagesToCopy)) {
1692
                continue;
1693
            }
1694
1695
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1696
                $fieldDefinition->fieldTypeIdentifier
1697
            );
1698
1699
            $newValue = $contentToPublish->getFieldValue(
1700
                $fieldDefinition->identifier,
1701
                $field->languageCode
1702
            );
1703
1704
            $value = $field->value;
1705
            if ($fieldDefinition->isRequired && $fieldType->isEmptyValue($value)) {
1706
                if (!$fieldType->isEmptyValue($fieldDefinition->defaultValue)) {
1707
                    $value = $fieldDefinition->defaultValue;
1708
                } else {
1709
                    $value = $contentToPublish->getFieldValue($field->fieldDefIdentifier, $versionInfo->initialLanguageCode);
1710
                }
1711
                $fallbackUpdateStruct->setField(
1712
                    $field->fieldDefIdentifier,
1713
                    $value,
1714
                    $field->languageCode
1715
                );
1716
                continue;
1717
            }
1718
1719
            if ($newValue !== null
1720
                && $field->value !== null
1721
                && $fieldType->toHash($newValue) === $fieldType->toHash($field->value)) {
1722
                continue;
1723
            }
1724
1725
            $updateStruct->setField($field->fieldDefIdentifier, $value, $field->languageCode);
1726
        }
1727
1728
        // Nothing to copy, skip update
1729
        if (empty($updateStruct->fields)) {
1730
            return;
1731
        }
1732
1733
        // Do fallback only if content needs to be updated
1734
        foreach ($fallbackUpdateStruct->fields as $fallbackField) {
1735
            $updateStruct->setField($fallbackField->fieldDefIdentifier, $fallbackField->value, $fallbackField->languageCode);
1736
        }
1737
1738
        $this->updateContent($versionInfo, $updateStruct);
1739
    }
1740
1741
    /**
1742
     * Publishes a content version.
1743
     *
1744
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1745
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1746
     *
1747
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1748
     *
1749
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1750
     * @param int|null $publicationDate If null existing date is kept if there is one, otherwise current time is used.
1751
     *
1752
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1753
     */
1754
    protected function internalPublishVersion(APIVersionInfo $versionInfo, $publicationDate = null)
1755
    {
1756
        if (!$versionInfo->isDraft()) {
1757
            throw new BadStateException('$versionInfo', 'Only versions in draft status can be published.');
1758
        }
1759
1760
        $currentTime = $this->getUnixTimestamp();
1761
        if ($publicationDate === null && $versionInfo->versionNo === 1) {
1762
            $publicationDate = $currentTime;
1763
        }
1764
1765
        $contentInfo = $versionInfo->getContentInfo();
1766
        $metadataUpdateStruct = new SPIMetadataUpdateStruct();
1767
        $metadataUpdateStruct->publicationDate = $publicationDate;
1768
        $metadataUpdateStruct->modificationDate = $currentTime;
1769
        $metadataUpdateStruct->isHidden = $contentInfo->isHidden;
1770
1771
        $contentId = $contentInfo->id;
1772
        $spiContent = $this->persistenceHandler->contentHandler()->publish(
1773
            $contentId,
1774
            $versionInfo->versionNo,
1775
            $metadataUpdateStruct
1776
        );
1777
1778
        $content = $this->domainMapper->buildContentDomainObject(
1779
            $spiContent,
1780
            $this->repository->getContentTypeService()->loadContentType(
1781
                $spiContent->versionInfo->contentInfo->contentTypeId
1782
            )
1783
        );
1784
1785
        $this->publishUrlAliasesForContent($content);
1786
1787
        // Delete version archive overflow if any, limit is 0-50 (however 0 will mean 1 if content is unpublished)
1788
        $archiveList = $this->persistenceHandler->contentHandler()->listVersions(
1789
            $contentId,
1790
            APIVersionInfo::STATUS_ARCHIVED,
1791
            100 // Limited to avoid publishing taking to long, besides SE limitations this is why limit is max 50
1792
        );
1793
1794
        $maxVersionArchiveCount = max(0, min(50, $this->settings['default_version_archive_limit']));
1795
        while (!empty($archiveList) && count($archiveList) > $maxVersionArchiveCount) {
1796
            /** @var \eZ\Publish\SPI\Persistence\Content\VersionInfo $archiveVersion */
1797
            $archiveVersion = array_shift($archiveList);
1798
            $this->persistenceHandler->contentHandler()->deleteVersion(
1799
                $contentId,
1800
                $archiveVersion->versionNo
1801
            );
1802
        }
1803
1804
        return $content;
1805
    }
1806
1807
    /**
1808
     * @return int
1809
     */
1810
    protected function getUnixTimestamp(): int
1811
    {
1812
        return time();
1813
    }
1814
1815
    /**
1816
     * Removes the given version.
1817
     *
1818
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is in
1819
     *         published state or is a last version of Content in non draft state
1820
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to remove this version
1821
     *
1822
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1823
     */
1824
    public function deleteVersion(APIVersionInfo $versionInfo): void
1825
    {
1826
        if ($versionInfo->isPublished()) {
1827
            throw new BadStateException(
1828
                '$versionInfo',
1829
                'Version is published and can not be removed'
1830
            );
1831
        }
1832
1833
        if (!$this->permissionResolver->canUser('content', 'versionremove', $versionInfo)) {
1834
            throw new UnauthorizedException(
1835
                'content',
1836
                'versionremove',
1837
                ['contentId' => $versionInfo->contentInfo->id, 'versionNo' => $versionInfo->versionNo]
1838
            );
1839
        }
1840
1841
        $versionList = $this->persistenceHandler->contentHandler()->listVersions(
1842
            $versionInfo->contentInfo->id,
1843
            null,
1844
            2
1845
        );
1846
1847
        if (count($versionList) === 1 && !$versionInfo->isDraft()) {
1848
            throw new BadStateException(
1849
                '$versionInfo',
1850
                'Version is the last version of the Content and can not be removed'
1851
            );
1852
        }
1853
1854
        $this->repository->beginTransaction();
1855
        try {
1856
            $this->persistenceHandler->contentHandler()->deleteVersion(
1857
                $versionInfo->getContentInfo()->id,
1858
                $versionInfo->versionNo
1859
            );
1860
            $this->repository->commit();
1861
        } catch (Exception $e) {
1862
            $this->repository->rollback();
1863
            throw $e;
1864
        }
1865
    }
1866
1867
    /**
1868
     * Loads all versions for the given content.
1869
     *
1870
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to list versions
1871
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the given status is invalid
1872
     *
1873
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1874
     * @param int|null $status
1875
     *
1876
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Sorted by creation date
1877
     */
1878
    public function loadVersions(ContentInfo $contentInfo, ?int $status = null): iterable
1879
    {
1880
        if (!$this->permissionResolver->canUser('content', 'versionread', $contentInfo)) {
1881
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentInfo->id]);
1882
        }
1883
1884
        if ($status !== null && !in_array((int)$status, [VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED], true)) {
1885
            throw new InvalidArgumentException(
1886
                'status',
1887
                sprintf(
1888
                    'it can be one of %d (draft), %d (published), %d (archived), %d given',
1889
                    VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED, $status
1890
                ));
1891
        }
1892
1893
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->listVersions($contentInfo->id, $status);
1894
1895
        $versions = [];
1896
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1897
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1898
            if (!$this->permissionResolver->canUser('content', 'versionread', $versionInfo)) {
1899
                throw new UnauthorizedException('content', 'versionread', ['versionId' => $versionInfo->id]);
1900
            }
1901
1902
            $versions[] = $versionInfo;
1903
        }
1904
1905
        return $versions;
1906
    }
1907
1908
    /**
1909
     * Copies the content to a new location. If no version is given,
1910
     * all versions are copied, otherwise only the given version.
1911
     *
1912
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to copy the content to the given location
1913
     *
1914
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1915
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $destinationLocationCreateStruct the target location where the content is copied to
1916
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1917
     *
1918
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1919
     */
1920
    public function copyContent(ContentInfo $contentInfo, LocationCreateStruct $destinationLocationCreateStruct, ?APIVersionInfo $versionInfo = null): APIContent
1921
    {
1922
        $destinationLocation = $this->repository->getLocationService()->loadLocation(
1923
            $destinationLocationCreateStruct->parentLocationId
1924
        );
1925
        if (!$this->permissionResolver->canUser('content', 'create', $contentInfo, [$destinationLocation])) {
1926
            throw new UnauthorizedException(
1927
                'content',
1928
                'create',
1929
                [
1930
                    'parentLocationId' => $destinationLocationCreateStruct->parentLocationId,
1931
                    'sectionId' => $contentInfo->sectionId,
1932
                ]
1933
            );
1934
        }
1935
        if (!$this->permissionResolver->canUser('content', 'manage_locations', $contentInfo, [$destinationLocation])) {
1936
            throw new UnauthorizedException('content', 'manage_locations', ['contentId' => $contentInfo->id]);
1937
        }
1938
1939
        $defaultObjectStates = $this->getDefaultObjectStates();
1940
1941
        $this->repository->beginTransaction();
1942
        try {
1943
            $spiContent = $this->persistenceHandler->contentHandler()->copy(
1944
                $contentInfo->id,
1945
                $versionInfo ? $versionInfo->versionNo : null,
1946
                $this->permissionResolver->getCurrentUserReference()->getUserId()
1947
            );
1948
1949
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
1950
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
1951
                $objectStateHandler->setContentState(
1952
                    $spiContent->versionInfo->contentInfo->id,
1953
                    $objectStateGroupId,
1954
                    $objectState->id
1955
                );
1956
            }
1957
1958
            $content = $this->internalPublishVersion(
1959
                $this->domainMapper->buildVersionInfoDomainObject($spiContent->versionInfo),
1960
                $spiContent->versionInfo->creationDate
1961
            );
1962
1963
            $this->repository->getLocationService()->createLocation(
1964
                $content->getVersionInfo()->getContentInfo(),
1965
                $destinationLocationCreateStruct
1966
            );
1967
            $this->repository->commit();
1968
        } catch (Exception $e) {
1969
            $this->repository->rollback();
1970
            throw $e;
1971
        }
1972
1973
        return $this->internalLoadContentById($content->id);
1974
    }
1975
1976
    /**
1977
     * Loads all outgoing relations for the given version.
1978
     *
1979
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1980
     *
1981
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1982
     *
1983
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1984
     */
1985
    public function loadRelations(APIVersionInfo $versionInfo): iterable
1986
    {
1987
        if ($versionInfo->isPublished()) {
1988
            $function = 'read';
1989
        } else {
1990
            $function = 'versionread';
1991
        }
1992
1993
        if (!$this->permissionResolver->canUser('content', $function, $versionInfo)) {
1994
            throw new UnauthorizedException('content', $function);
1995
        }
1996
1997
        $contentInfo = $versionInfo->getContentInfo();
1998
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
1999
            $contentInfo->id,
2000
            $versionInfo->versionNo
2001
        );
2002
2003
        /** @var $relations \eZ\Publish\API\Repository\Values\Content\Relation[] */
2004
        $relations = [];
2005
        foreach ($spiRelations as $spiRelation) {
2006
            $destinationContentInfo = $this->internalLoadContentInfoById($spiRelation->destinationContentId);
2007
            if (!$this->permissionResolver->canUser('content', 'read', $destinationContentInfo)) {
2008
                continue;
2009
            }
2010
2011
            $relations[] = $this->domainMapper->buildRelationDomainObject(
2012
                $spiRelation,
2013
                $contentInfo,
2014
                $destinationContentInfo
2015
            );
2016
        }
2017
2018
        return $relations;
2019
    }
2020
2021
    /**
2022
     * {@inheritdoc}
2023
     */
2024
    public function countReverseRelations(ContentInfo $contentInfo): int
2025
    {
2026
        if (!$this->permissionResolver->canUser('content', 'reverserelatedlist', $contentInfo)) {
2027
            return 0;
2028
        }
2029
2030
        return $this->persistenceHandler->contentHandler()->countReverseRelations(
2031
            $contentInfo->id
2032
        );
2033
    }
2034
2035
    /**
2036
     * Loads all incoming relations for a content object.
2037
     *
2038
     * The relations come only from published versions of the source content objects
2039
     *
2040
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
2041
     *
2042
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2043
     *
2044
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
2045
     */
2046
    public function loadReverseRelations(ContentInfo $contentInfo): iterable
2047
    {
2048
        if (!$this->permissionResolver->canUser('content', 'reverserelatedlist', $contentInfo)) {
2049
            throw new UnauthorizedException('content', 'reverserelatedlist', ['contentId' => $contentInfo->id]);
2050
        }
2051
2052
        $spiRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations(
2053
            $contentInfo->id
2054
        );
2055
2056
        $returnArray = [];
2057
        foreach ($spiRelations as $spiRelation) {
2058
            $sourceContentInfo = $this->internalLoadContentInfoById($spiRelation->sourceContentId);
2059
            if (!$this->permissionResolver->canUser('content', 'read', $sourceContentInfo)) {
2060
                continue;
2061
            }
2062
2063
            $returnArray[] = $this->domainMapper->buildRelationDomainObject(
2064
                $spiRelation,
2065
                $sourceContentInfo,
2066
                $contentInfo
2067
            );
2068
        }
2069
2070
        return $returnArray;
2071
    }
2072
2073
    /**
2074
     * {@inheritdoc}
2075
     */
2076
    public function loadReverseRelationList(ContentInfo $contentInfo, int $offset = 0, int $limit = -1): RelationList
2077
    {
2078
        $list = new RelationList();
2079
        if (!$this->repository->getPermissionResolver()->canUser('content', 'reverserelatedlist', $contentInfo)) {
2080
            return $list;
2081
        }
2082
2083
        $list->totalCount = $this->persistenceHandler->contentHandler()->countReverseRelations(
2084
            $contentInfo->id
2085
        );
2086
        if ($list->totalCount > 0) {
2087
            $spiRelationList = $this->persistenceHandler->contentHandler()->loadReverseRelationList(
2088
                $contentInfo->id,
2089
                $offset,
2090
                $limit
2091
            );
2092
            foreach ($spiRelationList as $spiRelation) {
2093
                $sourceContentInfo = $this->internalLoadContentInfoById($spiRelation->sourceContentId);
2094
                if ($this->repository->getPermissionResolver()->canUser('content', 'read', $sourceContentInfo)) {
2095
                    $relation = $this->domainMapper->buildRelationDomainObject(
2096
                        $spiRelation,
2097
                        $sourceContentInfo,
2098
                        $contentInfo
2099
                    );
2100
                    $list->items[] = new RelationListItem($relation);
2101
                } else {
2102
                    $list->items[] = new UnauthorizedRelationListItem(
2103
                        'content',
2104
                        'read',
2105
                        ['contentId' => $sourceContentInfo->id]
2106
                    );
2107
                }
2108
            }
2109
        }
2110
2111
        return $list;
2112
    }
2113
2114
    /**
2115
     * Adds a relation of type common.
2116
     *
2117
     * The source of the relation is the content and version
2118
     * referenced by $versionInfo.
2119
     *
2120
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to edit this version
2121
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2122
     *
2123
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2124
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent the destination of the relation
2125
     *
2126
     * @return \eZ\Publish\API\Repository\Values\Content\Relation the newly created relation
2127
     */
2128
    public function addRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent): APIRelation
2129
    {
2130
        $sourceVersion = $this->loadVersionInfoById(
2131
            $sourceVersion->contentInfo->id,
2132
            $sourceVersion->versionNo
2133
        );
2134
2135
        if (!$sourceVersion->isDraft()) {
2136
            throw new BadStateException(
2137
                '$sourceVersion',
2138
                'Relations of type common can only be added to versions of status draft'
2139
            );
2140
        }
2141
2142
        if (!$this->permissionResolver->canUser('content', 'edit', $sourceVersion)) {
2143
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2144
        }
2145
2146
        $sourceContentInfo = $sourceVersion->getContentInfo();
2147
2148
        $this->repository->beginTransaction();
2149
        try {
2150
            $spiRelation = $this->persistenceHandler->contentHandler()->addRelation(
2151
                new SPIRelationCreateStruct(
2152
                    [
2153
                        'sourceContentId' => $sourceContentInfo->id,
2154
                        'sourceContentVersionNo' => $sourceVersion->versionNo,
2155
                        'sourceFieldDefinitionId' => null,
2156
                        'destinationContentId' => $destinationContent->id,
2157
                        'type' => APIRelation::COMMON,
2158
                    ]
2159
                )
2160
            );
2161
            $this->repository->commit();
2162
        } catch (Exception $e) {
2163
            $this->repository->rollback();
2164
            throw $e;
2165
        }
2166
2167
        return $this->domainMapper->buildRelationDomainObject($spiRelation, $sourceContentInfo, $destinationContent);
2168
    }
2169
2170
    /**
2171
     * Removes a relation of type COMMON from a draft.
2172
     *
2173
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed edit this version
2174
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2175
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is no relation of type COMMON for the given destination
2176
     *
2177
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2178
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent
2179
     */
2180
    public function deleteRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent): void
2181
    {
2182
        $sourceVersion = $this->loadVersionInfoById(
2183
            $sourceVersion->contentInfo->id,
2184
            $sourceVersion->versionNo
2185
        );
2186
2187
        if (!$sourceVersion->isDraft()) {
2188
            throw new BadStateException(
2189
                '$sourceVersion',
2190
                'Relations of type common can only be removed from versions of status draft'
2191
            );
2192
        }
2193
2194
        if (!$this->permissionResolver->canUser('content', 'edit', $sourceVersion)) {
2195
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2196
        }
2197
2198
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
2199
            $sourceVersion->getContentInfo()->id,
2200
            $sourceVersion->versionNo,
2201
            APIRelation::COMMON
2202
        );
2203
2204
        if (empty($spiRelations)) {
2205
            throw new InvalidArgumentException(
2206
                '$sourceVersion',
2207
                'There are no relations of type COMMON for the given destination'
2208
            );
2209
        }
2210
2211
        // there should be only one relation of type COMMON for each destination,
2212
        // but in case there were ever more then one, we will remove them all
2213
        // @todo: alternatively, throw BadStateException?
2214
        $this->repository->beginTransaction();
2215
        try {
2216
            foreach ($spiRelations as $spiRelation) {
2217
                if ($spiRelation->destinationContentId == $destinationContent->id) {
2218
                    $this->persistenceHandler->contentHandler()->removeRelation(
2219
                        $spiRelation->id,
2220
                        APIRelation::COMMON
2221
                    );
2222
                }
2223
            }
2224
            $this->repository->commit();
2225
        } catch (Exception $e) {
2226
            $this->repository->rollback();
2227
            throw $e;
2228
        }
2229
    }
2230
2231
    /**
2232
     * {@inheritdoc}
2233
     */
2234
    public function removeTranslation(ContentInfo $contentInfo, string $languageCode): void
2235
    {
2236
        @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...
2237
            __METHOD__ . ' is deprecated, use deleteTranslation instead',
2238
            E_USER_DEPRECATED
2239
        );
2240
        $this->deleteTranslation($contentInfo, $languageCode);
2241
    }
2242
2243
    /**
2244
     * Delete Content item Translation from all Versions (including archived ones) of a Content Object.
2245
     *
2246
     * NOTE: this operation is risky and permanent, so user interface should provide a warning before performing it.
2247
     *
2248
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2249
     *         is the Main Translation of a Content Item.
2250
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2251
     *         to delete the content (in one of the locations of the given Content Item).
2252
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2253
     *         is invalid for the given content.
2254
     *
2255
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2256
     * @param string $languageCode
2257
     *
2258
     * @since 6.13
2259
     */
2260
    public function deleteTranslation(ContentInfo $contentInfo, string $languageCode): void
2261
    {
2262
        if ($contentInfo->mainLanguageCode === $languageCode) {
2263
            throw new BadStateException(
2264
                '$languageCode',
2265
                'Specified translation is the main translation of the Content Object'
2266
            );
2267
        }
2268
2269
        $translationWasFound = false;
2270
        $this->repository->beginTransaction();
2271
        try {
2272
            foreach ($this->loadVersions($contentInfo) as $versionInfo) {
2273
                if (!$this->permissionResolver->canUser('content', 'remove', $versionInfo)) {
2274
                    throw new UnauthorizedException(
2275
                        'content',
2276
                        'remove',
2277
                        ['contentId' => $contentInfo->id, 'versionNo' => $versionInfo->versionNo]
2278
                    );
2279
                }
2280
2281
                if (!in_array($languageCode, $versionInfo->languageCodes)) {
2282
                    continue;
2283
                }
2284
2285
                $translationWasFound = true;
2286
2287
                // If the translation is the version's only one, delete the version
2288
                if (count($versionInfo->languageCodes) < 2) {
2289
                    $this->persistenceHandler->contentHandler()->deleteVersion(
2290
                        $versionInfo->getContentInfo()->id,
2291
                        $versionInfo->versionNo
2292
                    );
2293
                }
2294
            }
2295
2296
            if (!$translationWasFound) {
2297
                throw new InvalidArgumentException(
2298
                    '$languageCode',
2299
                    sprintf(
2300
                        '%s does not exist in the Content item(id=%d)',
2301
                        $languageCode,
2302
                        $contentInfo->id
2303
                    )
2304
                );
2305
            }
2306
2307
            $this->persistenceHandler->contentHandler()->deleteTranslationFromContent(
2308
                $contentInfo->id,
2309
                $languageCode
2310
            );
2311
            $locationIds = array_map(
2312
                function (Location $location) {
2313
                    return $location->id;
2314
                },
2315
                $this->repository->getLocationService()->loadLocations($contentInfo)
2316
            );
2317
            $this->persistenceHandler->urlAliasHandler()->translationRemoved(
2318
                $locationIds,
2319
                $languageCode
2320
            );
2321
            $this->repository->commit();
2322
        } catch (InvalidArgumentException $e) {
2323
            $this->repository->rollback();
2324
            throw $e;
2325
        } catch (BadStateException $e) {
2326
            $this->repository->rollback();
2327
            throw $e;
2328
        } catch (UnauthorizedException $e) {
2329
            $this->repository->rollback();
2330
            throw $e;
2331
        } catch (Exception $e) {
2332
            $this->repository->rollback();
2333
            // cover generic unexpected exception to fulfill API promise on @throws
2334
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2335
        }
2336
    }
2337
2338
    /**
2339
     * Delete specified Translation from a Content Draft.
2340
     *
2341
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2342
     *         is the only one the Content Draft has or it is the main Translation of a Content Object.
2343
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2344
     *         to edit the Content (in one of the locations of the given Content Object).
2345
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2346
     *         is invalid for the given Draft.
2347
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if specified Version was not found
2348
     *
2349
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo Content Version Draft
2350
     * @param string $languageCode Language code of the Translation to be removed
2351
     *
2352
     * @return \eZ\Publish\API\Repository\Values\Content\Content Content Draft w/o the specified Translation
2353
     *
2354
     * @since 6.12
2355
     */
2356
    public function deleteTranslationFromDraft(APIVersionInfo $versionInfo, string $languageCode): APIContent
2357
    {
2358
        if (!$versionInfo->isDraft()) {
2359
            throw new BadStateException(
2360
                '$versionInfo',
2361
                'Version is not a draft, so Translations cannot be modified. Create a Draft before proceeding'
2362
            );
2363
        }
2364
2365
        if ($versionInfo->contentInfo->mainLanguageCode === $languageCode) {
2366
            throw new BadStateException(
2367
                '$languageCode',
2368
                'Specified Translation is the main Translation of the Content Object. Change it before proceeding.'
2369
            );
2370
        }
2371
2372
        if (!$this->permissionResolver->canUser('content', 'edit', $versionInfo->contentInfo)) {
2373
            throw new UnauthorizedException(
2374
                'content', 'edit', ['contentId' => $versionInfo->contentInfo->id]
2375
            );
2376
        }
2377
2378
        if (!in_array($languageCode, $versionInfo->languageCodes)) {
2379
            throw new InvalidArgumentException(
2380
                '$languageCode',
2381
                sprintf(
2382
                    'The Version (ContentId=%d, VersionNo=%d) is not translated into %s',
2383
                    $versionInfo->contentInfo->id,
2384
                    $versionInfo->versionNo,
2385
                    $languageCode
2386
                )
2387
            );
2388
        }
2389
2390
        if (count($versionInfo->languageCodes) === 1) {
2391
            throw new BadStateException(
2392
                '$languageCode',
2393
                'Specified Translation is the only one Content Object Version has'
2394
            );
2395
        }
2396
2397
        $this->repository->beginTransaction();
2398
        try {
2399
            $spiContent = $this->persistenceHandler->contentHandler()->deleteTranslationFromDraft(
2400
                $versionInfo->contentInfo->id,
2401
                $versionInfo->versionNo,
2402
                $languageCode
2403
            );
2404
            $this->repository->commit();
2405
2406
            return $this->domainMapper->buildContentDomainObject(
2407
                $spiContent,
2408
                $this->repository->getContentTypeService()->loadContentType(
2409
                    $spiContent->versionInfo->contentInfo->contentTypeId
2410
                )
2411
            );
2412
        } catch (APINotFoundException $e) {
2413
            // avoid wrapping expected NotFoundException in BadStateException handled below
2414
            $this->repository->rollback();
2415
            throw $e;
2416
        } catch (Exception $e) {
2417
            $this->repository->rollback();
2418
            // cover generic unexpected exception to fulfill API promise on @throws
2419
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2420
        }
2421
    }
2422
2423
    /**
2424
     * Hides Content by making all the Locations appear hidden.
2425
     * It does not persist hidden state on Location object itself.
2426
     *
2427
     * Content hidden by this API can be revealed by revealContent API.
2428
     *
2429
     * @see revealContent
2430
     *
2431
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2432
     */
2433
    public function hideContent(ContentInfo $contentInfo): void
2434
    {
2435
        if (!$this->permissionResolver->canUser('content', 'hide', $contentInfo)) {
2436
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2437
        }
2438
2439
        $this->repository->beginTransaction();
2440
        try {
2441
            $this->persistenceHandler->contentHandler()->updateMetadata(
2442
                $contentInfo->id,
2443
                new SPIMetadataUpdateStruct([
2444
                    'isHidden' => true,
2445
                ])
2446
            );
2447
            $locationHandler = $this->persistenceHandler->locationHandler();
2448
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2449
            foreach ($childLocations as $childLocation) {
2450
                $locationHandler->setInvisible($childLocation->id);
2451
            }
2452
            $this->repository->commit();
2453
        } catch (Exception $e) {
2454
            $this->repository->rollback();
2455
            throw $e;
2456
        }
2457
    }
2458
2459
    /**
2460
     * Reveals Content hidden by hideContent API.
2461
     * Locations which were hidden before hiding Content will remain hidden.
2462
     *
2463
     * @see hideContent
2464
     *
2465
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2466
     */
2467
    public function revealContent(ContentInfo $contentInfo): void
2468
    {
2469
        if (!$this->permissionResolver->canUser('content', 'hide', $contentInfo)) {
2470
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2471
        }
2472
2473
        $this->repository->beginTransaction();
2474
        try {
2475
            $this->persistenceHandler->contentHandler()->updateMetadata(
2476
                $contentInfo->id,
2477
                new SPIMetadataUpdateStruct([
2478
                    'isHidden' => false,
2479
                ])
2480
            );
2481
            $locationHandler = $this->persistenceHandler->locationHandler();
2482
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2483
            foreach ($childLocations as $childLocation) {
2484
                $locationHandler->setVisible($childLocation->id);
2485
            }
2486
            $this->repository->commit();
2487
        } catch (Exception $e) {
2488
            $this->repository->rollback();
2489
            throw $e;
2490
        }
2491
    }
2492
2493
    /**
2494
     * Instantiates a new content create struct object.
2495
     *
2496
     * alwaysAvailable is set to the ContentType's defaultAlwaysAvailable
2497
     *
2498
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
2499
     * @param string $mainLanguageCode
2500
     *
2501
     * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
2502
     */
2503
    public function newContentCreateStruct(ContentType $contentType, string $mainLanguageCode): APIContentCreateStruct
2504
    {
2505
        return new ContentCreateStruct(
2506
            [
2507
                'contentType' => $contentType,
2508
                'mainLanguageCode' => $mainLanguageCode,
2509
                'alwaysAvailable' => $contentType->defaultAlwaysAvailable,
2510
            ]
2511
        );
2512
    }
2513
2514
    /**
2515
     * Instantiates a new content meta data update struct.
2516
     *
2517
     * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
2518
     */
2519
    public function newContentMetadataUpdateStruct(): ContentMetadataUpdateStruct
2520
    {
2521
        return new ContentMetadataUpdateStruct();
2522
    }
2523
2524
    /**
2525
     * Instantiates a new content update struct.
2526
     *
2527
     * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
2528
     */
2529
    public function newContentUpdateStruct(): APIContentUpdateStruct
2530
    {
2531
        return new ContentUpdateStruct();
2532
    }
2533
2534
    /**
2535
     * @param \eZ\Publish\API\Repository\Values\User\User|null $user
2536
     *
2537
     * @return \eZ\Publish\API\Repository\Values\User\UserReference
2538
     */
2539
    private function resolveUser(?User $user): UserReference
2540
    {
2541
        if ($user === null) {
2542
            $user = $this->permissionResolver->getCurrentUserReference();
2543
        }
2544
2545
        return $user;
2546
    }
2547
}
2548