Completed
Push — ezp_30973 ( feb262...9f83f6 )
by
unknown
14:39
created

ContentService::deleteRelation()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

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