Completed
Push — ezp_30981_content_info_proxy ( a78a98...0757d2 )
by
unknown
15:21
created

ContentService::updateContent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 27
rs 9.488
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\Content;
23
use eZ\Publish\Core\Repository\Values\Content\Location;
24
use eZ\Publish\API\Repository\Values\Content\Language;
25
use eZ\Publish\SPI\Persistence\Handler;
26
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
27
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
28
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
29
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
30
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
31
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
32
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
33
use eZ\Publish\API\Repository\Values\User\User;
34
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
35
use eZ\Publish\API\Repository\Values\Content\Field;
36
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
37
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
38
use eZ\Publish\Core\Base\Exceptions\BadStateException;
39
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
40
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
41
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
42
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
43
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
44
use eZ\Publish\Core\FieldType\ValidationError;
45
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
46
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
47
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
48
use eZ\Publish\SPI\Limitation\Target;
49
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
50
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
51
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
52
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
53
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
54
use Exception;
55
56
/**
57
 * This class provides service methods for managing content.
58
 *
59
 * @example Examples/content.php
60
 */
61
class ContentService implements ContentServiceInterface
62
{
63
    /** @var \eZ\Publish\Core\Repository\Repository */
64
    protected $repository;
65
66
    /** @var \eZ\Publish\SPI\Persistence\Handler */
67
    protected $persistenceHandler;
68
69
    /** @var array */
70
    protected $settings;
71
72
    /** @var \eZ\Publish\Core\Repository\Helper\DomainMapper */
73
    protected $domainMapper;
74
75
    /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
76
    protected $relationProcessor;
77
78
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
79
    protected $nameSchemaService;
80
81
    /** @var \eZ\Publish\Core\FieldType\FieldTypeRegistry */
82
    protected $fieldTypeRegistry;
83
84
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
85
    private $permissionResolver;
86
87
    public function __construct(
88
        RepositoryInterface $repository,
89
        Handler $handler,
90
        Helper\DomainMapper $domainMapper,
91
        Helper\RelationProcessor $relationProcessor,
92
        Helper\NameSchemaService $nameSchemaService,
93
        FieldTypeRegistry $fieldTypeRegistry,
94
        PermissionResolver $permissionResolver,
95
        array $settings = []
96
    ) {
97
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repository of type object<eZ\Publish\API\Repository\Repository> is incompatible with the declared type object<eZ\Publish\Core\Repository\Repository> of property $repository.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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