Completed
Push — content_service_typehint ( ae59a1...5fa235 )
by
unknown
38:00 queued 22:39
created

ContentService::internalLoadContentById()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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