Completed
Push — content_service_typehint ( 5fa235...668285 )
by
unknown
14:46
created

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