Completed
Push — master ( 3b0f4c...c1c946 )
by
unknown
39:12 queued 08:03
created

copyTranslationsFromPublishedVersion()   D

Complexity

Conditions 17
Paths 39

Size

Total Lines 94

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
nc 39
nop 2
dl 0
loc 94
rs 4.3042
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * File containing the eZ\Publish\Core\Repository\ContentService class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Repository;
10
11
use eZ\Publish\API\Repository\ContentService as ContentServiceInterface;
12
use eZ\Publish\API\Repository\PermissionResolver;
13
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
14
use eZ\Publish\Core\FieldType\FieldTypeRegistry;
15
use eZ\Publish\API\Repository\Values\Content\ContentDraftList;
16
use eZ\Publish\API\Repository\Values\Content\DraftList\Item\ContentDraftListItem;
17
use eZ\Publish\API\Repository\Values\Content\DraftList\Item\UnauthorizedContentDraftListItem;
18
use eZ\Publish\API\Repository\Values\Content\RelationList;
19
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\RelationListItem;
20
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\UnauthorizedRelationListItem;
21
use eZ\Publish\API\Repository\Values\User\UserReference;
22
use eZ\Publish\Core\Repository\Values\Content\Location;
23
use eZ\Publish\API\Repository\Values\Content\Language;
24
use eZ\Publish\SPI\Persistence\Handler;
25
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
26
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
27
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
28
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
29
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
30
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
31
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
32
use eZ\Publish\API\Repository\Values\User\User;
33
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
34
use eZ\Publish\API\Repository\Values\Content\Field;
35
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
36
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
37
use eZ\Publish\Core\Base\Exceptions\BadStateException;
38
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
39
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
40
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
41
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
42
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
43
use eZ\Publish\Core\FieldType\ValidationError;
44
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
45
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
46
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
47
use eZ\Publish\SPI\Limitation\Target;
48
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
49
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
50
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
51
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
52
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
53
use Exception;
54
55
/**
56
 * This class provides service methods for managing content.
57
 *
58
 * @example Examples/content.php
59
 */
60
class ContentService implements ContentServiceInterface
61
{
62
    /** @var \eZ\Publish\Core\Repository\Repository */
63
    protected $repository;
64
65
    /** @var \eZ\Publish\SPI\Persistence\Handler */
66
    protected $persistenceHandler;
67
68
    /** @var array */
69
    protected $settings;
70
71
    /** @var \eZ\Publish\Core\Repository\Helper\DomainMapper */
72
    protected $domainMapper;
73
74
    /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
75
    protected $relationProcessor;
76
77
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
78
    protected $nameSchemaService;
79
80
    /** @var \eZ\Publish\Core\FieldType\FieldTypeRegistry */
81
    protected $fieldTypeRegistry;
82
83
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
84
    private $permissionResolver;
85
86
    public function __construct(
87
        RepositoryInterface $repository,
88
        Handler $handler,
89
        Helper\DomainMapper $domainMapper,
90
        Helper\RelationProcessor $relationProcessor,
91
        Helper\NameSchemaService $nameSchemaService,
92
        FieldTypeRegistry $fieldTypeRegistry,
93
        PermissionResolver $permissionResolver,
94
        array $settings = []
95
    ) {
96
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repository of type object<eZ\Publish\API\Repository\Repository> is incompatible with the declared type object<eZ\Publish\Core\Repository\Repository> of property $repository.

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

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

Loading history...
97
        $this->persistenceHandler = $handler;
98
        $this->domainMapper = $domainMapper;
99
        $this->relationProcessor = $relationProcessor;
100
        $this->nameSchemaService = $nameSchemaService;
101
        $this->fieldTypeRegistry = $fieldTypeRegistry;
102
        // Union makes sure default settings are ignored if provided in argument
103
        $this->settings = $settings + [
104
            // Version archive limit (0-50), only enforced on publish, not on un-publish.
105
            'default_version_archive_limit' => 5,
106
        ];
107
        $this->permissionResolver = $permissionResolver;
108
    }
109
110
    /**
111
     * Loads a content info object.
112
     *
113
     * To load fields use loadContent
114
     *
115
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
116
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
117
     *
118
     * @param int $contentId
119
     *
120
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
121
     */
122
    public function loadContentInfo($contentId)
123
    {
124
        $contentInfo = $this->internalLoadContentInfo($contentId);
125
        if (!$this->permissionResolver->canUser('content', 'read', $contentInfo)) {
126
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
127
        }
128
129
        return $contentInfo;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function loadContentInfoList(array $contentIds): iterable
136
    {
137
        $contentInfoList = [];
138
        $spiInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
139
        foreach ($spiInfoList as $id => $spiInfo) {
140
            $contentInfo = $this->domainMapper->buildContentInfoDomainObject($spiInfo);
141
            if ($this->permissionResolver->canUser('content', 'read', $contentInfo)) {
142
                $contentInfoList[$id] = $contentInfo;
143
            }
144
        }
145
146
        return $contentInfoList;
147
    }
148
149
    /**
150
     * Loads a content info object.
151
     *
152
     * To load fields use loadContent
153
     *
154
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
155
     *
156
     * @param mixed $id
157
     * @param bool $isRemoteId
158
     *
159
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
160
     */
161
    public function internalLoadContentInfo($id, $isRemoteId = false)
162
    {
163
        try {
164
            $method = $isRemoteId ? 'loadContentInfoByRemoteId' : 'loadContentInfo';
165
166
            return $this->domainMapper->buildContentInfoDomainObject(
167
                $this->persistenceHandler->contentHandler()->$method($id)
168
            );
169
        } catch (APINotFoundException $e) {
170
            throw new NotFoundException(
171
                'Content',
172
                $id,
173
                $e
174
            );
175
        }
176
    }
177
178
    /**
179
     * Loads a content info object for the given remoteId.
180
     *
181
     * To load fields use loadContent
182
     *
183
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
184
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given remote id does not exist
185
     *
186
     * @param string $remoteId
187
     *
188
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
189
     */
190
    public function loadContentInfoByRemoteId($remoteId)
191
    {
192
        $contentInfo = $this->internalLoadContentInfo($remoteId, true);
193
194
        if (!$this->permissionResolver->canUser('content', 'read', $contentInfo)) {
195
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
196
        }
197
198
        return $contentInfo;
199
    }
200
201
    /**
202
     * Loads a version info of the given content object.
203
     *
204
     * If no version number is given, the method returns the current version
205
     *
206
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
207
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
208
     *
209
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
210
     * @param int $versionNo the version number. If not given the current version is returned.
211
     *
212
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
213
     */
214
    public function loadVersionInfo(ContentInfo $contentInfo, $versionNo = null)
215
    {
216
        return $this->loadVersionInfoById($contentInfo->id, $versionNo);
217
    }
218
219
    /**
220
     * Loads a version info of the given content object id.
221
     *
222
     * If no version number is given, the method returns the current version
223
     *
224
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
225
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
226
     *
227
     * @param mixed $contentId
228
     * @param int $versionNo the version number. If not given the current version is returned.
229
     *
230
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
231
     */
232
    public function loadVersionInfoById($contentId, $versionNo = null)
233
    {
234
        try {
235
            $spiVersionInfo = $this->persistenceHandler->contentHandler()->loadVersionInfo(
236
                $contentId,
237
                $versionNo
238
            );
239
        } catch (APINotFoundException $e) {
240
            throw new NotFoundException(
241
                'VersionInfo',
242
                [
243
                    'contentId' => $contentId,
244
                    'versionNo' => $versionNo,
245
                ],
246
                $e
247
            );
248
        }
249
250
        $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
251
252
        if ($versionInfo->isPublished()) {
253
            $function = 'read';
254
        } else {
255
            $function = 'versionread';
256
        }
257
258
        if (!$this->permissionResolver->canUser('content', $function, $versionInfo)) {
259
            throw new UnauthorizedException('content', $function, ['contentId' => $contentId]);
260
        }
261
262
        return $versionInfo;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
269
    {
270
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
271
        if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
272
            $useAlwaysAvailable = false;
273
        }
274
275
        return $this->loadContent(
276
            $contentInfo->id,
277
            $languages,
278
            $versionNo,// On purpose pass as-is and not use $contentInfo, to make sure to return actual current version on null
279
            $useAlwaysAvailable
280
        );
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages = null, $useAlwaysAvailable = true)
287
    {
288
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
289
        if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
290
            $useAlwaysAvailable = false;
291
        }
292
293
        return $this->loadContent(
294
            $versionInfo->getContentInfo()->id,
295
            $languages,
296
            $versionInfo->versionNo,
297
            $useAlwaysAvailable
298
        );
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function loadContent($contentId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
305
    {
306
        $content = $this->internalLoadContent($contentId, $languages, $versionNo, false, $useAlwaysAvailable);
307
308
        if (!$this->permissionResolver->canUser('content', 'read', $content)) {
309
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
310
        }
311
        if (
312
            !$content->getVersionInfo()->isPublished()
313
            && !$this->permissionResolver->canUser('content', 'versionread', $content)
314
        ) {
315
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId, 'versionNo' => $versionNo]);
316
        }
317
318
        return $content;
319
    }
320
321
    /**
322
     * Loads content in a version of the given content object.
323
     *
324
     * If no version number is given, the method returns the current version
325
     *
326
     * @internal
327
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the content or version with the given id and languages does not exist
328
     *
329
     * @param mixed $id
330
     * @param array|null $languages A language priority, filters returned fields and is used as prioritized language code on
331
     *                         returned value object. If not given all languages are returned.
332
     * @param int|null $versionNo the version number. If not given the current version is returned
333
     * @param bool $isRemoteId
334
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
335
     *
336
     * @return \eZ\Publish\API\Repository\Values\Content\Content
337
     */
338
    public function internalLoadContent($id, array $languages = null, $versionNo = null, $isRemoteId = false, $useAlwaysAvailable = true)
339
    {
340
        try {
341
            // Get Content ID if lookup by remote ID
342
            if ($isRemoteId) {
343
                $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($id);
344
                $id = $spiContentInfo->id;
345
                // Set $isRemoteId to false as the next loads will be for content id now that we have it (for exception use now)
346
                $isRemoteId = false;
347
            }
348
349
            $loadLanguages = $languages;
350
            $alwaysAvailableLanguageCode = null;
351
            // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
352
            // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
353
            if (!empty($loadLanguages) && $useAlwaysAvailable) {
354
                if (!isset($spiContentInfo)) {
355
                    $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
356
                }
357
358
                if ($spiContentInfo->alwaysAvailable) {
359
                    $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
360
                    $loadLanguages = array_unique($loadLanguages);
361
                }
362
            }
363
364
            $spiContent = $this->persistenceHandler->contentHandler()->load(
365
                $id,
366
                $versionNo,
367
                $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 349 can also be of type array; however, eZ\Publish\SPI\Persistence\Content\Handler::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
368
            );
369
        } catch (APINotFoundException $e) {
370
            throw new NotFoundException(
371
                'Content',
372
                [
373
                    $isRemoteId ? 'remoteId' : 'id' => $id,
374
                    'languages' => $languages,
375
                    'versionNo' => $versionNo,
376
                ],
377
                $e
378
            );
379
        }
380
381
        if ($languages === null) {
382
            $languages = [];
383
        }
384
385
        return $this->domainMapper->buildContentDomainObject(
386
            $spiContent,
387
            $this->repository->getContentTypeService()->loadContentType(
388
                $spiContent->versionInfo->contentInfo->contentTypeId,
389
                $languages
390
            ),
391
            $languages,
392
            $alwaysAvailableLanguageCode
393
        );
394
    }
395
396
    /**
397
     * Loads content in a version for the content object reference by the given remote id.
398
     *
399
     * If no version is given, the method returns the current version
400
     *
401
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content or version with the given remote id does not exist
402
     * @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
403
     *
404
     * @param string $remoteId
405
     * @param array $languages A language filter for fields. If not given all languages are returned
406
     * @param int $versionNo the version number. If not given the current version is returned
407
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
408
     *
409
     * @return \eZ\Publish\API\Repository\Values\Content\Content
410
     */
411
    public function loadContentByRemoteId($remoteId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
412
    {
413
        $content = $this->internalLoadContent($remoteId, $languages, $versionNo, true, $useAlwaysAvailable);
414
415
        if (!$this->permissionResolver->canUser('content', 'read', $content)) {
416
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
417
        }
418
419
        if (
420
            !$content->getVersionInfo()->isPublished()
421
            && !$this->permissionResolver->canUser('content', 'versionread', $content)
422
        ) {
423
            throw new UnauthorizedException('content', 'versionread', ['remoteId' => $remoteId, 'versionNo' => $versionNo]);
424
        }
425
426
        return $content;
427
    }
428
429
    /**
430
     * Bulk-load Content items by the list of ContentInfo Value Objects.
431
     *
432
     * Note: it does not throw exceptions on load, just ignores erroneous Content item.
433
     * Moreover, since the method works on pre-loaded ContentInfo list, it is assumed that user is
434
     * allowed to access every Content on the list.
435
     *
436
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo[] $contentInfoList
437
     * @param string[] $languages A language priority, filters returned fields and is used as prioritized language code on
438
     *                            returned value object. If not given all languages are returned.
439
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true,
440
     *                                 unless all languages have been asked for.
441
     *
442
     * @return \eZ\Publish\API\Repository\Values\Content\Content[] list of Content items with Content Ids as keys
443
     */
444
    public function loadContentListByContentInfo(
445
        array $contentInfoList,
446
        array $languages = [],
447
        $useAlwaysAvailable = true
448
    ) {
449
        $loadAllLanguages = $languages === Language::ALL;
450
        $contentIds = [];
451
        $contentTypeIds = [];
452
        $translations = $languages;
453
        foreach ($contentInfoList as $contentInfo) {
454
            $contentIds[] = $contentInfo->id;
455
            $contentTypeIds[] = $contentInfo->contentTypeId;
456
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
457
            // Might in some case load more languages then intended, but prioritised handling will pick right one
458
            if (!$loadAllLanguages && $useAlwaysAvailable && $contentInfo->alwaysAvailable) {
459
                $translations[] = $contentInfo->mainLanguageCode;
460
            }
461
        }
462
463
        $contentList = [];
464
        $translations = array_unique($translations);
465
        $spiContentList = $this->persistenceHandler->contentHandler()->loadContentList(
466
            $contentIds,
467
            $translations
468
        );
469
        $contentTypeList = $this->repository->getContentTypeService()->loadContentTypeList(
470
            array_unique($contentTypeIds),
471
            $languages
472
        );
473
        foreach ($spiContentList as $contentId => $spiContent) {
474
            $contentInfo = $spiContent->versionInfo->contentInfo;
475
            $contentList[$contentId] = $this->domainMapper->buildContentDomainObject(
476
                $spiContent,
477
                $contentTypeList[$contentInfo->contentTypeId],
478
                $languages,
479
                $contentInfo->alwaysAvailable ? $contentInfo->mainLanguageCode : null
480
            );
481
        }
482
483
        return $contentList;
484
    }
485
486
    /**
487
     * Creates a new content draft assigned to the authenticated user.
488
     *
489
     * If a different userId is given in $contentCreateStruct it is assigned to the given user
490
     * but this required special rights for the authenticated user
491
     * (this is useful for content staging where the transfer process does not
492
     * have to authenticate with the user which created the content object in the source server).
493
     * The user has to publish the draft if it should be visible.
494
     * In 4.x at least one location has to be provided in the location creation array.
495
     *
496
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to create the content in the given location
497
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the provided remoteId exists in the system, required properties on
498
     *                                                                        struct are missing or invalid, or if multiple locations are under the
499
     *                                                                        same parent.
500
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
501
     *                                                                               or if a required field is missing / set to an empty value.
502
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
503
     *                                                                          or value is set for non-translatable field in language
504
     *                                                                          other than main.
505
     *
506
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
507
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs For each location parent under which a location should be created for the content
508
     *
509
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
510
     */
511
    public function createContent(APIContentCreateStruct $contentCreateStruct, array $locationCreateStructs = [])
512
    {
513
        if ($contentCreateStruct->mainLanguageCode === null) {
514
            throw new InvalidArgumentException('$contentCreateStruct', "'mainLanguageCode' property must be set");
515
        }
516
517
        if ($contentCreateStruct->contentType === null) {
518
            throw new InvalidArgumentException('$contentCreateStruct', "'contentType' property must be set");
519
        }
520
521
        $contentCreateStruct = clone $contentCreateStruct;
522
523
        if ($contentCreateStruct->ownerId === null) {
524
            $contentCreateStruct->ownerId = $this->permissionResolver->getCurrentUserReference()->getUserId();
525
        }
526
527
        if ($contentCreateStruct->alwaysAvailable === null) {
528
            $contentCreateStruct->alwaysAvailable = $contentCreateStruct->contentType->defaultAlwaysAvailable ?: false;
529
        }
530
531
        $contentCreateStruct->contentType = $this->repository->getContentTypeService()->loadContentType(
532
            $contentCreateStruct->contentType->id
533
        );
534
535
        if (empty($contentCreateStruct->sectionId)) {
536
            if (isset($locationCreateStructs[0])) {
537
                $location = $this->repository->getLocationService()->loadLocation(
538
                    $locationCreateStructs[0]->parentLocationId
539
                );
540
                $contentCreateStruct->sectionId = $location->contentInfo->sectionId;
541
            } else {
542
                $contentCreateStruct->sectionId = 1;
543
            }
544
        }
545
546
        if (!$this->permissionResolver->canUser('content', 'create', $contentCreateStruct, $locationCreateStructs)) {
547
            throw new UnauthorizedException(
548
                'content',
549
                'create',
550
                [
551
                    'parentLocationId' => isset($locationCreateStructs[0]) ?
552
                            $locationCreateStructs[0]->parentLocationId :
553
                            null,
554
                    'sectionId' => $contentCreateStruct->sectionId,
555
                ]
556
            );
557
        }
558
559
        if (!empty($contentCreateStruct->remoteId)) {
560
            try {
561
                $this->loadContentByRemoteId($contentCreateStruct->remoteId);
562
563
                throw new InvalidArgumentException(
564
                    '$contentCreateStruct',
565
                    "Another content with remoteId '{$contentCreateStruct->remoteId}' exists"
566
                );
567
            } catch (APINotFoundException $e) {
568
                // Do nothing
569
            }
570
        } else {
571
            $contentCreateStruct->remoteId = $this->domainMapper->getUniqueHash($contentCreateStruct);
572
        }
573
574
        $spiLocationCreateStructs = $this->buildSPILocationCreateStructs($locationCreateStructs);
575
576
        $languageCodes = $this->getLanguageCodesForCreate($contentCreateStruct);
577
        $fields = $this->mapFieldsForCreate($contentCreateStruct);
578
579
        $fieldValues = [];
580
        $spiFields = [];
581
        $allFieldErrors = [];
582
        $inputRelations = [];
583
        $locationIdToContentIdMapping = [];
584
585
        foreach ($contentCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
586
            /** @var $fieldType \eZ\Publish\Core\FieldType\FieldType */
587
            $fieldType = $this->fieldTypeRegistry->getFieldType(
588
                $fieldDefinition->fieldTypeIdentifier
589
            );
590
591
            foreach ($languageCodes as $languageCode) {
592
                $isEmptyValue = false;
593
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $contentCreateStruct->mainLanguageCode;
594
                $isLanguageMain = $languageCode === $contentCreateStruct->mainLanguageCode;
595
                if (isset($fields[$fieldDefinition->identifier][$valueLanguageCode])) {
596
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
597
                } else {
598
                    $fieldValue = $fieldDefinition->defaultValue;
599
                }
600
601
                $fieldValue = $fieldType->acceptValue($fieldValue);
602
603
                if ($fieldType->isEmptyValue($fieldValue)) {
604
                    $isEmptyValue = true;
605
                    if ($fieldDefinition->isRequired) {
606
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
607
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
608
                            null,
609
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
610
                            'empty'
611
                        );
612
                    }
613
                } else {
614
                    $fieldErrors = $fieldType->validate(
615
                        $fieldDefinition,
616
                        $fieldValue
617
                    );
618
                    if (!empty($fieldErrors)) {
619
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
620
                    }
621
                }
622
623
                if (!empty($allFieldErrors)) {
624
                    continue;
625
                }
626
627
                $this->relationProcessor->appendFieldRelations(
628
                    $inputRelations,
629
                    $locationIdToContentIdMapping,
630
                    $fieldType,
631
                    $fieldValue,
632
                    $fieldDefinition->id
633
                );
634
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
635
636
                // Only non-empty value for: translatable field or in main language
637
                if (
638
                    (!$isEmptyValue && $fieldDefinition->isTranslatable) ||
639
                    (!$isEmptyValue && $isLanguageMain)
640
                ) {
641
                    $spiFields[] = new SPIField(
642
                        [
643
                            'id' => null,
644
                            'fieldDefinitionId' => $fieldDefinition->id,
645
                            'type' => $fieldDefinition->fieldTypeIdentifier,
646
                            'value' => $fieldType->toPersistenceValue($fieldValue),
647
                            'languageCode' => $languageCode,
648
                            'versionNo' => null,
649
                        ]
650
                    );
651
                }
652
            }
653
        }
654
655
        if (!empty($allFieldErrors)) {
656
            throw new ContentFieldValidationException($allFieldErrors);
657
        }
658
659
        $spiContentCreateStruct = new SPIContentCreateStruct(
660
            [
661
                'name' => $this->nameSchemaService->resolve(
662
                    $contentCreateStruct->contentType->nameSchema,
663
                    $contentCreateStruct->contentType,
664
                    $fieldValues,
665
                    $languageCodes
666
                ),
667
                'typeId' => $contentCreateStruct->contentType->id,
668
                'sectionId' => $contentCreateStruct->sectionId,
669
                'ownerId' => $contentCreateStruct->ownerId,
670
                'locations' => $spiLocationCreateStructs,
671
                'fields' => $spiFields,
672
                'alwaysAvailable' => $contentCreateStruct->alwaysAvailable,
673
                'remoteId' => $contentCreateStruct->remoteId,
674
                'modified' => isset($contentCreateStruct->modificationDate) ? $contentCreateStruct->modificationDate->getTimestamp() : time(),
675
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
676
                    $contentCreateStruct->mainLanguageCode
677
                )->id,
678
            ]
679
        );
680
681
        $defaultObjectStates = $this->getDefaultObjectStates();
682
683
        $this->repository->beginTransaction();
684
        try {
685
            $spiContent = $this->persistenceHandler->contentHandler()->create($spiContentCreateStruct);
686
            $this->relationProcessor->processFieldRelations(
687
                $inputRelations,
688
                $spiContent->versionInfo->contentInfo->id,
689
                $spiContent->versionInfo->versionNo,
690
                $contentCreateStruct->contentType
691
            );
692
693
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
694
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
695
                $objectStateHandler->setContentState(
696
                    $spiContent->versionInfo->contentInfo->id,
697
                    $objectStateGroupId,
698
                    $objectState->id
699
                );
700
            }
701
702
            $this->repository->commit();
703
        } catch (Exception $e) {
704
            $this->repository->rollback();
705
            throw $e;
706
        }
707
708
        return $this->domainMapper->buildContentDomainObject(
709
            $spiContent,
710
            $contentCreateStruct->contentType
711
        );
712
    }
713
714
    /**
715
     * Returns an array of default content states with content state group id as key.
716
     *
717
     * @return \eZ\Publish\SPI\Persistence\Content\ObjectState[]
718
     */
719
    protected function getDefaultObjectStates()
720
    {
721
        $defaultObjectStatesMap = [];
722
        $objectStateHandler = $this->persistenceHandler->objectStateHandler();
723
724
        foreach ($objectStateHandler->loadAllGroups() as $objectStateGroup) {
725
            foreach ($objectStateHandler->loadObjectStates($objectStateGroup->id) as $objectState) {
726
                // Only register the first object state which is the default one.
727
                $defaultObjectStatesMap[$objectStateGroup->id] = $objectState;
728
                break;
729
            }
730
        }
731
732
        return $defaultObjectStatesMap;
733
    }
734
735
    /**
736
     * Returns all language codes used in given $fields.
737
     *
738
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value is set in main language
739
     *
740
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
741
     *
742
     * @return string[]
743
     */
744
    protected function getLanguageCodesForCreate(APIContentCreateStruct $contentCreateStruct)
745
    {
746
        $languageCodes = [];
747
748
        foreach ($contentCreateStruct->fields as $field) {
749
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
750
                continue;
751
            }
752
753
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
754
                $field->languageCode
755
            );
756
            $languageCodes[$field->languageCode] = true;
757
        }
758
759
        if (!isset($languageCodes[$contentCreateStruct->mainLanguageCode])) {
760
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
761
                $contentCreateStruct->mainLanguageCode
762
            );
763
            $languageCodes[$contentCreateStruct->mainLanguageCode] = true;
764
        }
765
766
        return array_keys($languageCodes);
767
    }
768
769
    /**
770
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
771
     *
772
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
773
     *                                                                          or value is set for non-translatable field in language
774
     *                                                                          other than main
775
     *
776
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
777
     *
778
     * @return array
779
     */
780
    protected function mapFieldsForCreate(APIContentCreateStruct $contentCreateStruct)
781
    {
782
        $fields = [];
783
784
        foreach ($contentCreateStruct->fields as $field) {
785
            $fieldDefinition = $contentCreateStruct->contentType->getFieldDefinition($field->fieldDefIdentifier);
786
787
            if ($fieldDefinition === null) {
788
                throw new ContentValidationException(
789
                    "Field definition '%identifier%' does not exist in given ContentType",
790
                    ['%identifier%' => $field->fieldDefIdentifier]
791
                );
792
            }
793
794
            if ($field->languageCode === null) {
795
                $field = $this->cloneField(
796
                    $field,
797
                    ['languageCode' => $contentCreateStruct->mainLanguageCode]
798
                );
799
            }
800
801
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $contentCreateStruct->mainLanguageCode)) {
802
                throw new ContentValidationException(
803
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
804
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
805
                );
806
            }
807
808
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
809
        }
810
811
        return $fields;
812
    }
813
814
    /**
815
     * Clones $field with overriding specific properties from given $overrides array.
816
     *
817
     * @param Field $field
818
     * @param array $overrides
819
     *
820
     * @return Field
821
     */
822
    private function cloneField(Field $field, array $overrides = [])
823
    {
824
        $fieldData = array_merge(
825
            [
826
                'id' => $field->id,
827
                'value' => $field->value,
828
                'languageCode' => $field->languageCode,
829
                'fieldDefIdentifier' => $field->fieldDefIdentifier,
830
                'fieldTypeIdentifier' => $field->fieldTypeIdentifier,
831
            ],
832
            $overrides
833
        );
834
835
        return new Field($fieldData);
836
    }
837
838
    /**
839
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
840
     *
841
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs
842
     *
843
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct[]
844
     */
845
    protected function buildSPILocationCreateStructs(array $locationCreateStructs)
846
    {
847
        $spiLocationCreateStructs = [];
848
        $parentLocationIdSet = [];
849
        $mainLocation = true;
850
851
        foreach ($locationCreateStructs as $locationCreateStruct) {
852
            if (isset($parentLocationIdSet[$locationCreateStruct->parentLocationId])) {
853
                throw new InvalidArgumentException(
854
                    '$locationCreateStructs',
855
                    "Multiple LocationCreateStructs with the same parent Location '{$locationCreateStruct->parentLocationId}' are given"
856
                );
857
            }
858
859
            if (!array_key_exists($locationCreateStruct->sortField, Location::SORT_FIELD_MAP)) {
860
                $locationCreateStruct->sortField = Location::SORT_FIELD_NAME;
861
            }
862
863
            if (!array_key_exists($locationCreateStruct->sortOrder, Location::SORT_ORDER_MAP)) {
864
                $locationCreateStruct->sortOrder = Location::SORT_ORDER_ASC;
865
            }
866
867
            $parentLocationIdSet[$locationCreateStruct->parentLocationId] = true;
868
            $parentLocation = $this->repository->getLocationService()->loadLocation(
869
                $locationCreateStruct->parentLocationId
870
            );
871
872
            $spiLocationCreateStructs[] = $this->domainMapper->buildSPILocationCreateStruct(
873
                $locationCreateStruct,
874
                $parentLocation,
875
                $mainLocation,
876
                // For Content draft contentId and contentVersionNo are set in ContentHandler upon draft creation
877
                null,
878
                null
879
            );
880
881
            // First Location in the list will be created as main Location
882
            $mainLocation = false;
883
        }
884
885
        return $spiLocationCreateStructs;
886
    }
887
888
    /**
889
     * Updates the metadata.
890
     *
891
     * (see {@link ContentMetadataUpdateStruct}) of a content object - to update fields use updateContent
892
     *
893
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update the content meta data
894
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the remoteId in $contentMetadataUpdateStruct is set but already exists
895
     *
896
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
897
     * @param \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct $contentMetadataUpdateStruct
898
     *
899
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content with the updated attributes
900
     */
901
    public function updateContentMetadata(ContentInfo $contentInfo, ContentMetadataUpdateStruct $contentMetadataUpdateStruct)
902
    {
903
        $propertyCount = 0;
904
        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...
905
            if (isset($contentMetadataUpdateStruct->$propertyName)) {
906
                $propertyCount += 1;
907
            }
908
        }
909
        if ($propertyCount === 0) {
910
            throw new InvalidArgumentException(
911
                '$contentMetadataUpdateStruct',
912
                'At least one property must be set'
913
            );
914
        }
915
916
        $loadedContentInfo = $this->loadContentInfo($contentInfo->id);
917
918
        if (!$this->permissionResolver->canUser('content', 'edit', $loadedContentInfo)) {
919
            throw new UnauthorizedException('content', 'edit', ['contentId' => $loadedContentInfo->id]);
920
        }
921
922
        if (isset($contentMetadataUpdateStruct->remoteId)) {
923
            try {
924
                $existingContentInfo = $this->loadContentInfoByRemoteId($contentMetadataUpdateStruct->remoteId);
925
926
                if ($existingContentInfo->id !== $loadedContentInfo->id) {
927
                    throw new InvalidArgumentException(
928
                        '$contentMetadataUpdateStruct',
929
                        "Another content with remoteId '{$contentMetadataUpdateStruct->remoteId}' exists"
930
                    );
931
                }
932
            } catch (APINotFoundException $e) {
933
                // Do nothing
934
            }
935
        }
936
937
        $this->repository->beginTransaction();
938
        try {
939
            if ($propertyCount > 1 || !isset($contentMetadataUpdateStruct->mainLocationId)) {
940
                $this->persistenceHandler->contentHandler()->updateMetadata(
941
                    $loadedContentInfo->id,
942
                    new SPIMetadataUpdateStruct(
943
                        [
944
                            'ownerId' => $contentMetadataUpdateStruct->ownerId,
945
                            'publicationDate' => isset($contentMetadataUpdateStruct->publishedDate) ?
946
                                $contentMetadataUpdateStruct->publishedDate->getTimestamp() :
947
                                null,
948
                            'modificationDate' => isset($contentMetadataUpdateStruct->modificationDate) ?
949
                                $contentMetadataUpdateStruct->modificationDate->getTimestamp() :
950
                                null,
951
                            'mainLanguageId' => isset($contentMetadataUpdateStruct->mainLanguageCode) ?
952
                                $this->repository->getContentLanguageService()->loadLanguage(
953
                                    $contentMetadataUpdateStruct->mainLanguageCode
954
                                )->id :
955
                                null,
956
                            'alwaysAvailable' => $contentMetadataUpdateStruct->alwaysAvailable,
957
                            'remoteId' => $contentMetadataUpdateStruct->remoteId,
958
                            'name' => $contentMetadataUpdateStruct->name,
959
                        ]
960
                    )
961
                );
962
            }
963
964
            // Change main location
965
            if (isset($contentMetadataUpdateStruct->mainLocationId)
966
                && $loadedContentInfo->mainLocationId !== $contentMetadataUpdateStruct->mainLocationId) {
967
                $this->persistenceHandler->locationHandler()->changeMainLocation(
968
                    $loadedContentInfo->id,
969
                    $contentMetadataUpdateStruct->mainLocationId
970
                );
971
            }
972
973
            // Republish URL aliases to update always-available flag
974
            if (isset($contentMetadataUpdateStruct->alwaysAvailable)
975
                && $loadedContentInfo->alwaysAvailable !== $contentMetadataUpdateStruct->alwaysAvailable) {
976
                $content = $this->loadContent($loadedContentInfo->id);
977
                $this->publishUrlAliasesForContent($content, false);
978
            }
979
980
            $this->repository->commit();
981
        } catch (Exception $e) {
982
            $this->repository->rollback();
983
            throw $e;
984
        }
985
986
        return isset($content) ? $content : $this->loadContent($loadedContentInfo->id);
987
    }
988
989
    /**
990
     * Publishes URL aliases for all locations of a given content.
991
     *
992
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
993
     * @param bool $updatePathIdentificationString this parameter is legacy storage specific for updating
994
     *                      ezcontentobject_tree.path_identification_string, it is ignored by other storage engines
995
     */
996
    protected function publishUrlAliasesForContent(APIContent $content, $updatePathIdentificationString = true)
997
    {
998
        $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
999
        $locations = $this->repository->getLocationService()->loadLocations(
1000
            $content->getVersionInfo()->getContentInfo()
1001
        );
1002
        $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
1003
        foreach ($locations as $location) {
1004
            foreach ($urlAliasNames as $languageCode => $name) {
1005
                $urlAliasHandler->publishUrlAliasForLocation(
1006
                    $location->id,
1007
                    $location->parentLocationId,
1008
                    $name,
1009
                    $languageCode,
1010
                    $content->contentInfo->alwaysAvailable,
1011
                    $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...
1012
                );
1013
            }
1014
            // archive URL aliases of Translations that got deleted
1015
            $urlAliasHandler->archiveUrlAliasesForDeletedTranslations(
1016
                $location->id,
1017
                $location->parentLocationId,
1018
                $content->versionInfo->languageCodes
1019
            );
1020
        }
1021
    }
1022
1023
    /**
1024
     * Deletes a content object including all its versions and locations including their subtrees.
1025
     *
1026
     * @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)
1027
     *
1028
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1029
     *
1030
     * @return mixed[] Affected Location Id's
1031
     */
1032
    public function deleteContent(ContentInfo $contentInfo)
1033
    {
1034
        $contentInfo = $this->internalLoadContentInfo($contentInfo->id);
1035
1036
        if (!$this->permissionResolver->canUser('content', 'remove', $contentInfo)) {
1037
            throw new UnauthorizedException('content', 'remove', ['contentId' => $contentInfo->id]);
1038
        }
1039
1040
        $affectedLocations = [];
1041
        $this->repository->beginTransaction();
1042
        try {
1043
            // Load Locations first as deleting Content also deletes belonging Locations
1044
            $spiLocations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentInfo->id);
1045
            $this->persistenceHandler->contentHandler()->deleteContent($contentInfo->id);
1046
            $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
1047
            foreach ($spiLocations as $spiLocation) {
1048
                $urlAliasHandler->locationDeleted($spiLocation->id);
1049
                $affectedLocations[] = $spiLocation->id;
1050
            }
1051
            $this->repository->commit();
1052
        } catch (Exception $e) {
1053
            $this->repository->rollback();
1054
            throw $e;
1055
        }
1056
1057
        return $affectedLocations;
1058
    }
1059
1060
    /**
1061
     * Creates a draft from a published or archived version.
1062
     *
1063
     * If no version is given, the current published version is used.
1064
     *
1065
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1066
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1067
     * @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
1068
     * @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.
1069
     *
1070
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
1071
     *
1072
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
1073
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the current-user is not allowed to create the draft
1074
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to create the draft
1075
     */
1076
    public function createContentDraft(
1077
        ContentInfo $contentInfo,
1078
        APIVersionInfo $versionInfo = null,
1079
        User $creator = null,
1080
        ?Language $language = null
1081
    ) {
1082
        $contentInfo = $this->loadContentInfo($contentInfo->id);
1083
1084
        if ($versionInfo !== null) {
1085
            // Check that given $contentInfo and $versionInfo belong to the same content
1086
            if ($versionInfo->getContentInfo()->id != $contentInfo->id) {
1087
                throw new InvalidArgumentException(
1088
                    '$versionInfo',
1089
                    'VersionInfo does not belong to the same content as given ContentInfo'
1090
                );
1091
            }
1092
1093
            $versionInfo = $this->loadVersionInfoById($contentInfo->id, $versionInfo->versionNo);
1094
1095
            switch ($versionInfo->status) {
1096
                case VersionInfo::STATUS_PUBLISHED:
1097
                case VersionInfo::STATUS_ARCHIVED:
1098
                    break;
1099
1100
                default:
1101
                    // @todo: throw an exception here, to be defined
1102
                    throw new BadStateException(
1103
                        '$versionInfo',
1104
                        'Draft can not be created from a draft version'
1105
                    );
1106
            }
1107
1108
            $versionNo = $versionInfo->versionNo;
1109
        } elseif ($contentInfo->published) {
1110
            $versionNo = $contentInfo->currentVersionNo;
1111
        } else {
1112
            // @todo: throw an exception here, to be defined
1113
            throw new BadStateException(
1114
                '$contentInfo',
1115
                'Content is not published, draft can be created only from published or archived version'
1116
            );
1117
        }
1118
1119
        if ($creator === null) {
1120
            $creator = $this->permissionResolver->getCurrentUserReference();
1121
        }
1122
1123
        $fallbackLanguageCode = $versionInfo->initialLanguageCode ?? $contentInfo->mainLanguageCode;
1124
        $languageCode = $language->languageCode ?? $fallbackLanguageCode;
1125
1126
        if (!$this->permissionResolver->canUser(
1127
            'content',
1128
            'edit',
1129
            $contentInfo,
1130
            [
1131
                (new Target\Builder\VersionBuilder())
1132
                    ->changeStatusTo(APIVersionInfo::STATUS_DRAFT)
1133
                    ->build(),
1134
            ]
1135
        )) {
1136
            throw new UnauthorizedException(
1137
                'content',
1138
                'edit',
1139
                ['contentId' => $contentInfo->id]
1140
            );
1141
        }
1142
1143
        $this->repository->beginTransaction();
1144
        try {
1145
            $spiContent = $this->persistenceHandler->contentHandler()->createDraftFromVersion(
1146
                $contentInfo->id,
1147
                $versionNo,
1148
                $creator->getUserId(),
1149
                $languageCode
1150
            );
1151
            $this->repository->commit();
1152
        } catch (Exception $e) {
1153
            $this->repository->rollback();
1154
            throw $e;
1155
        }
1156
1157
        return $this->domainMapper->buildContentDomainObject(
1158
            $spiContent,
1159
            $this->repository->getContentTypeService()->loadContentType(
1160
                $spiContent->versionInfo->contentInfo->contentTypeId
1161
            )
1162
        );
1163
    }
1164
1165
    public function countContentDrafts(?User $user = null): int
1166
    {
1167
        if ($this->permissionResolver->hasAccess('content', 'versionread') === false) {
1168
            return 0;
1169
        }
1170
1171
        return $this->persistenceHandler->contentHandler()->countDraftsForUser(
1172
            $this->resolveUser($user)->getUserId()
1173
        );
1174
    }
1175
1176
    /**
1177
     * Loads drafts for a user.
1178
     *
1179
     * If no user is given the drafts for the authenticated user are returned
1180
     *
1181
     * @param \eZ\Publish\API\Repository\Values\User\User|null $user
1182
     *
1183
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Drafts owned by the given user
1184
     *
1185
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1186
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1187
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1188
     */
1189
    public function loadContentDrafts(User $user = null)
1190
    {
1191
        // throw early if user has absolutely no access to versionread
1192
        if ($this->permissionResolver->hasAccess('content', 'versionread') === false) {
1193
            throw new UnauthorizedException('content', 'versionread');
1194
        }
1195
1196
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftsForUser(
1197
            $this->resolveUser($user)->getUserId()
1198
        );
1199
        $versionInfoList = [];
1200
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1201
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1202
            // @todo: Change this to filter returned drafts by permissions instead of throwing
1203
            if (!$this->permissionResolver->canUser('content', 'versionread', $versionInfo)) {
1204
                throw new UnauthorizedException('content', 'versionread', ['contentId' => $versionInfo->contentInfo->id]);
1205
            }
1206
1207
            $versionInfoList[] = $versionInfo;
1208
        }
1209
1210
        return $versionInfoList;
1211
    }
1212
1213
    public function loadContentDraftList(?User $user = null, int $offset = 0, int $limit = -1): ContentDraftList
1214
    {
1215
        $list = new ContentDraftList();
1216
        if ($this->permissionResolver->hasAccess('content', 'versionread') === false) {
1217
            return $list;
1218
        }
1219
1220
        $list->totalCount = $this->persistenceHandler->contentHandler()->countDraftsForUser(
1221
            $this->resolveUser($user)->getUserId()
1222
        );
1223
        if ($list->totalCount > 0) {
1224
            $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftListForUser(
1225
                $this->resolveUser($user)->getUserId(),
1226
                $offset,
1227
                $limit
1228
            );
1229
            foreach ($spiVersionInfoList as $spiVersionInfo) {
1230
                $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1231
                if ($this->permissionResolver->canUser('content', 'versionread', $versionInfo)) {
1232
                    $list->items[] = new ContentDraftListItem($versionInfo);
1233
                } else {
1234
                    $list->items[] = new UnauthorizedContentDraftListItem(
1235
                        'content',
1236
                        'versionread',
1237
                        ['contentId' => $versionInfo->contentInfo->id]
1238
                    );
1239
                }
1240
            }
1241
        }
1242
1243
        return $list;
1244
    }
1245
1246
    /**
1247
     * Updates the fields of a draft.
1248
     *
1249
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1250
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1251
     *
1252
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the updated fields
1253
     *
1254
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
1255
     *                                                                               or if a required field is missing / set to an empty value.
1256
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
1257
     *                                                                          or value is set for non-translatable field in language
1258
     *                                                                          other than main.
1259
     *
1260
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
1261
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1262
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
1263
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1264
     */
1265
    public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct)
1266
    {
1267
        $contentUpdateStruct = clone $contentUpdateStruct;
1268
1269
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1270
        $content = $this->loadContent(
1271
            $versionInfo->getContentInfo()->id,
1272
            null,
1273
            $versionInfo->versionNo
1274
        );
1275
        if (!$content->versionInfo->isDraft()) {
1276
            throw new BadStateException(
1277
                '$versionInfo',
1278
                'Version is not a draft and can not be updated'
1279
            );
1280
        }
1281
1282
        if (!$this->repository->getPermissionResolver()->canUser(
1283
            'content',
1284
            'edit',
1285
            $content,
1286
            [
1287
                (new Target\Builder\VersionBuilder())
1288
                    ->updateFieldsTo(
1289
                        $contentUpdateStruct->initialLanguageCode,
1290
                        $contentUpdateStruct->fields
1291
                    )
1292
                    ->build(),
1293
            ]
1294
        )) {
1295
            throw new UnauthorizedException('content', 'edit', ['contentId' => $content->id]);
1296
        }
1297
1298
        $mainLanguageCode = $content->contentInfo->mainLanguageCode;
1299
        if ($contentUpdateStruct->initialLanguageCode === null) {
1300
            $contentUpdateStruct->initialLanguageCode = $mainLanguageCode;
1301
        }
1302
1303
        $allLanguageCodes = $this->getLanguageCodesForUpdate($contentUpdateStruct, $content);
1304
        $contentLanguageHandler = $this->persistenceHandler->contentLanguageHandler();
1305
        foreach ($allLanguageCodes as $languageCode) {
1306
            $contentLanguageHandler->loadByLanguageCode($languageCode);
1307
        }
1308
1309
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1310
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1311
            $content->contentInfo->contentTypeId
1312
        );
1313
        $fields = $this->mapFieldsForUpdate(
1314
            $contentUpdateStruct,
1315
            $contentType,
1316
            $mainLanguageCode
1317
        );
1318
1319
        $fieldValues = [];
1320
        $spiFields = [];
1321
        $allFieldErrors = [];
1322
        $inputRelations = [];
1323
        $locationIdToContentIdMapping = [];
1324
1325
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1326
            /** @var $fieldType \eZ\Publish\SPI\FieldType\FieldType */
1327
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1328
                $fieldDefinition->fieldTypeIdentifier
1329
            );
1330
1331
            foreach ($allLanguageCodes as $languageCode) {
1332
                $isCopied = $isEmpty = $isRetained = false;
1333
                $isLanguageNew = !in_array($languageCode, $content->versionInfo->languageCodes);
1334
                $isLanguageUpdated = in_array($languageCode, $updatedLanguageCodes);
1335
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $mainLanguageCode;
1336
                $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
1337
                $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
1338
1339
                if (!$isFieldUpdated && !$isLanguageNew) {
1340
                    $isRetained = true;
1341
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1342
                } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
1343
                    $isCopied = true;
1344
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1345
                } elseif ($isFieldUpdated) {
1346
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
1347
                } else {
1348
                    $fieldValue = $fieldDefinition->defaultValue;
1349
                }
1350
1351
                $fieldValue = $fieldType->acceptValue($fieldValue);
1352
1353
                if ($fieldType->isEmptyValue($fieldValue)) {
1354
                    $isEmpty = true;
1355
                    if ($isLanguageUpdated && $fieldDefinition->isRequired) {
1356
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
1357
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
1358
                            null,
1359
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
1360
                            'empty'
1361
                        );
1362
                    }
1363
                } elseif ($isLanguageUpdated) {
1364
                    $fieldErrors = $fieldType->validate(
1365
                        $fieldDefinition,
1366
                        $fieldValue
1367
                    );
1368
                    if (!empty($fieldErrors)) {
1369
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
1370
                    }
1371
                }
1372
1373
                if (!empty($allFieldErrors)) {
1374
                    continue;
1375
                }
1376
1377
                $this->relationProcessor->appendFieldRelations(
1378
                    $inputRelations,
1379
                    $locationIdToContentIdMapping,
1380
                    $fieldType,
1381
                    $fieldValue,
1382
                    $fieldDefinition->id
1383
                );
1384
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
1385
1386
                if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
1387
                    continue;
1388
                }
1389
1390
                $spiFields[] = new SPIField(
1391
                    [
1392
                        'id' => $isLanguageNew ?
1393
                            null :
1394
                            $content->getField($fieldDefinition->identifier, $languageCode)->id,
1395
                        'fieldDefinitionId' => $fieldDefinition->id,
1396
                        'type' => $fieldDefinition->fieldTypeIdentifier,
1397
                        'value' => $fieldType->toPersistenceValue($fieldValue),
1398
                        'languageCode' => $languageCode,
1399
                        'versionNo' => $versionInfo->versionNo,
1400
                    ]
1401
                );
1402
            }
1403
        }
1404
1405
        if (!empty($allFieldErrors)) {
1406
            throw new ContentFieldValidationException($allFieldErrors);
1407
        }
1408
1409
        $spiContentUpdateStruct = new SPIContentUpdateStruct(
1410
            [
1411
                'name' => $this->nameSchemaService->resolveNameSchema(
1412
                    $content,
1413
                    $fieldValues,
1414
                    $allLanguageCodes,
1415
                    $contentType
1416
                ),
1417
                'creatorId' => $contentUpdateStruct->creatorId ?: $this->permissionResolver->getCurrentUserReference()->getUserId(),
1418
                'fields' => $spiFields,
1419
                'modificationDate' => time(),
1420
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1421
                    $contentUpdateStruct->initialLanguageCode
1422
                )->id,
1423
            ]
1424
        );
1425
        $existingRelations = $this->loadRelations($versionInfo);
1426
1427
        $this->repository->beginTransaction();
1428
        try {
1429
            $spiContent = $this->persistenceHandler->contentHandler()->updateContent(
1430
                $versionInfo->getContentInfo()->id,
1431
                $versionInfo->versionNo,
1432
                $spiContentUpdateStruct
1433
            );
1434
            $this->relationProcessor->processFieldRelations(
1435
                $inputRelations,
1436
                $spiContent->versionInfo->contentInfo->id,
1437
                $spiContent->versionInfo->versionNo,
1438
                $contentType,
1439
                $existingRelations
1440
            );
1441
            $this->repository->commit();
1442
        } catch (Exception $e) {
1443
            $this->repository->rollback();
1444
            throw $e;
1445
        }
1446
1447
        return $this->domainMapper->buildContentDomainObject(
1448
            $spiContent,
1449
            $contentType
1450
        );
1451
    }
1452
1453
    /**
1454
     * Returns only updated language codes.
1455
     *
1456
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1457
     *
1458
     * @return array
1459
     */
1460
    private function getUpdatedLanguageCodes(APIContentUpdateStruct $contentUpdateStruct)
1461
    {
1462
        $languageCodes = [
1463
            $contentUpdateStruct->initialLanguageCode => true,
1464
        ];
1465
1466
        foreach ($contentUpdateStruct->fields as $field) {
1467
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
1468
                continue;
1469
            }
1470
1471
            $languageCodes[$field->languageCode] = true;
1472
        }
1473
1474
        return array_keys($languageCodes);
1475
    }
1476
1477
    /**
1478
     * Returns all language codes used in given $fields.
1479
     *
1480
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value exists in initial language
1481
     *
1482
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1483
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1484
     *
1485
     * @return array
1486
     */
1487
    protected function getLanguageCodesForUpdate(APIContentUpdateStruct $contentUpdateStruct, APIContent $content)
1488
    {
1489
        $languageCodes = array_fill_keys($content->versionInfo->languageCodes, true);
1490
        $languageCodes[$contentUpdateStruct->initialLanguageCode] = true;
1491
1492
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1493
        foreach ($updatedLanguageCodes as $languageCode) {
1494
            $languageCodes[$languageCode] = true;
1495
        }
1496
1497
        return array_keys($languageCodes);
1498
    }
1499
1500
    /**
1501
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
1502
     *
1503
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
1504
     *                                                                          or value is set for non-translatable field in language
1505
     *                                                                          other than main
1506
     *
1507
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1508
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1509
     * @param string $mainLanguageCode
1510
     *
1511
     * @return array
1512
     */
1513
    protected function mapFieldsForUpdate(
1514
        APIContentUpdateStruct $contentUpdateStruct,
1515
        ContentType $contentType,
1516
        $mainLanguageCode
1517
    ) {
1518
        $fields = [];
1519
1520
        foreach ($contentUpdateStruct->fields as $field) {
1521
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1522
1523
            if ($fieldDefinition === null) {
1524
                throw new ContentValidationException(
1525
                    "Field definition '%identifier%' does not exist in given ContentType",
1526
                    ['%identifier%' => $field->fieldDefIdentifier]
1527
                );
1528
            }
1529
1530
            if ($field->languageCode === null) {
1531
                if ($fieldDefinition->isTranslatable) {
1532
                    $languageCode = $contentUpdateStruct->initialLanguageCode;
1533
                } else {
1534
                    $languageCode = $mainLanguageCode;
1535
                }
1536
                $field = $this->cloneField($field, ['languageCode' => $languageCode]);
1537
            }
1538
1539
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $mainLanguageCode)) {
1540
                throw new ContentValidationException(
1541
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
1542
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
1543
                );
1544
            }
1545
1546
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
1547
        }
1548
1549
        return $fields;
1550
    }
1551
1552
    /**
1553
     * Publishes a content version.
1554
     *
1555
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1556
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1557
     *
1558
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1559
     * @param string[] $translations
1560
     *
1561
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1562
     *
1563
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1564
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1565
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1566
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1567
     */
1568
    public function publishVersion(APIVersionInfo $versionInfo, array $translations = Language::ALL)
1569
    {
1570
        $content = $this->internalLoadContent(
1571
            $versionInfo->contentInfo->id,
1572
            null,
1573
            $versionInfo->versionNo
1574
        );
1575
1576
        $fromContent = null;
0 ignored issues
show
Unused Code introduced by
$fromContent is not used, you could remove the assignment.

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

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

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

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

Loading history...
1577
        if ($content->contentInfo->currentVersionNo !== $versionInfo->versionNo) {
1578
            $fromContent = $this->internalLoadContent(
1579
                $content->contentInfo->id,
1580
                null,
1581
                $content->contentInfo->currentVersionNo
1582
            );
1583
            // should not occur now, might occur in case of un-publish
1584
            if (!$fromContent->contentInfo->isPublished()) {
1585
                $fromContent = null;
0 ignored issues
show
Unused Code introduced by
$fromContent is not used, you could remove the assignment.

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

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

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

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

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