Completed
Push — ezp_30827 ( 031a3c...efb07b )
by
unknown
13:58
created

ContentService::loadContentDrafts()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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