Completed
Push — EZP-30427 ( f2742c...7c8d0e )
by
unknown
19:22
created

ContentService::loadContentByVersionInfo()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 3
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the eZ\Publish\Core\Repository\ContentService class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Repository;
10
11
use eZ\Publish\API\Repository\ContentService as ContentServiceInterface;
12
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
13
use eZ\Publish\API\Repository\Values\Content\ContentDraftList;
14
use eZ\Publish\API\Repository\Values\Content\DraftList\Item\ContentDraftListItem;
15
use eZ\Publish\API\Repository\Values\Content\DraftList\Item\UnauthorizedContentDraftListItem;
16
use eZ\Publish\API\Repository\Values\User\UserReference;
17
use eZ\Publish\Core\Repository\Values\Content\Location;
18
use eZ\Publish\API\Repository\Values\Content\Language;
19
use eZ\Publish\SPI\Persistence\Handler;
20
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
21
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
22
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
23
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
24
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
25
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
26
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
27
use eZ\Publish\API\Repository\Values\User\User;
28
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
29
use eZ\Publish\API\Repository\Values\Content\Field;
30
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
31
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
32
use eZ\Publish\Core\Base\Exceptions\BadStateException;
33
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
34
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
35
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
36
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
37
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
38
use eZ\Publish\Core\FieldType\ValidationError;
39
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
40
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
41
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
42
use eZ\Publish\SPI\Limitation\Target;
43
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
44
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
45
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
46
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
47
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
48
use Exception;
49
50
/**
51
 * This class provides service methods for managing content.
52
 *
53
 * @example Examples/content.php
54
 */
55
class ContentService implements ContentServiceInterface
56
{
57
    /** @var \eZ\Publish\Core\Repository\Repository */
58
    protected $repository;
59
60
    /** @var \eZ\Publish\SPI\Persistence\Handler */
61
    protected $persistenceHandler;
62
63
    /** @var array */
64
    protected $settings;
65
66
    /** @var \eZ\Publish\Core\Repository\Helper\DomainMapper */
67
    protected $domainMapper;
68
69
    /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
70
    protected $relationProcessor;
71
72
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
73
    protected $nameSchemaService;
74
75
    /** @var \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry */
76
    protected $fieldTypeRegistry;
77
78
    /**
79
     * Setups service with reference to repository object that created it & corresponding handler.
80
     *
81
     * @param \eZ\Publish\API\Repository\Repository $repository
82
     * @param \eZ\Publish\SPI\Persistence\Handler $handler
83
     * @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
84
     * @param \eZ\Publish\Core\Repository\Helper\RelationProcessor $relationProcessor
85
     * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
86
     * @param \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry $fieldTypeRegistry,
0 ignored issues
show
Documentation introduced by
There is no parameter named $fieldTypeRegistry,. Did you maybe mean $fieldTypeRegistry?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
87
     * @param array $settings
88
     */
89
    public function __construct(
90
        RepositoryInterface $repository,
91
        Handler $handler,
92
        Helper\DomainMapper $domainMapper,
93
        Helper\RelationProcessor $relationProcessor,
94
        Helper\NameSchemaService $nameSchemaService,
95
        Helper\FieldTypeRegistry $fieldTypeRegistry,
96
        array $settings = []
97
    ) {
98
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
$repository is of type object<eZ\Publish\API\Repository\Repository>, but the property $repository was declared to be of type object<eZ\Publish\Core\Repository\Repository>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
99
        $this->persistenceHandler = $handler;
100
        $this->domainMapper = $domainMapper;
101
        $this->relationProcessor = $relationProcessor;
102
        $this->nameSchemaService = $nameSchemaService;
103
        $this->fieldTypeRegistry = $fieldTypeRegistry;
104
        // Union makes sure default settings are ignored if provided in argument
105
        $this->settings = $settings + [
106
            // Version archive limit (0-50), only enforced on publish, not on un-publish.
107
            'default_version_archive_limit' => 5,
108
        ];
109
    }
110
111
    /**
112
     * Loads a content info object.
113
     *
114
     * To load fields use loadContent
115
     *
116
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
117
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
118
     *
119
     * @param int $contentId
120
     *
121
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
122
     */
123
    public function loadContentInfo($contentId)
124
    {
125
        $contentInfo = $this->internalLoadContentInfo($contentId);
126
        if (!$this->repository->canUser('content', 'read', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
127
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
128
        }
129
130
        return $contentInfo;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function loadContentInfoList(array $contentIds): iterable
137
    {
138
        $contentInfoList = [];
139
        $spiInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
140
        foreach ($spiInfoList as $id => $spiInfo) {
141
            $contentInfo = $this->domainMapper->buildContentInfoDomainObject($spiInfo);
142
            if ($this->repository->canUser('content', 'read', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
143
                $contentInfoList[$id] = $contentInfo;
144
            }
145
        }
146
147
        return $contentInfoList;
148
    }
149
150
    /**
151
     * Loads a content info object.
152
     *
153
     * To load fields use loadContent
154
     *
155
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
156
     *
157
     * @param mixed $id
158
     * @param bool $isRemoteId
159
     *
160
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
161
     */
162
    public function internalLoadContentInfo($id, $isRemoteId = false)
163
    {
164
        try {
165
            $method = $isRemoteId ? 'loadContentInfoByRemoteId' : 'loadContentInfo';
166
167
            return $this->domainMapper->buildContentInfoDomainObject(
168
                $this->persistenceHandler->contentHandler()->$method($id)
169
            );
170
        } catch (APINotFoundException $e) {
171
            throw new NotFoundException(
172
                'Content',
173
                $id,
174
                $e
175
            );
176
        }
177
    }
178
179
    /**
180
     * Loads a content info object for the given remoteId.
181
     *
182
     * To load fields use loadContent
183
     *
184
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
185
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given remote id does not exist
186
     *
187
     * @param string $remoteId
188
     *
189
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
190
     */
191
    public function loadContentInfoByRemoteId($remoteId)
192
    {
193
        $contentInfo = $this->internalLoadContentInfo($remoteId, true);
194
195
        if (!$this->repository->canUser('content', 'read', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
196
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
197
        }
198
199
        return $contentInfo;
200
    }
201
202
    /**
203
     * Loads a version info of the given content object.
204
     *
205
     * If no version number is given, the method returns the current version
206
     *
207
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
208
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
209
     *
210
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
211
     * @param int $versionNo the version number. If not given the current version is returned.
212
     *
213
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
214
     */
215
    public function loadVersionInfo(ContentInfo $contentInfo, $versionNo = null)
216
    {
217
        return $this->loadVersionInfoById($contentInfo->id, $versionNo);
218
    }
219
220
    /**
221
     * Loads a version info of the given content object id.
222
     *
223
     * If no version number is given, the method returns the current version
224
     *
225
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
226
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
227
     *
228
     * @param mixed $contentId
229
     * @param int $versionNo the version number. If not given the current version is returned.
230
     *
231
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
232
     */
233
    public function loadVersionInfoById($contentId, $versionNo = null)
234
    {
235
        // @todo SPI should also support null to avoid concurrency issues
236
        if ($versionNo === null) {
237
            $versionNo = $this->loadContentInfo($contentId)->currentVersionNo;
238
        }
239
240
        try {
241
            $spiVersionInfo = $this->persistenceHandler->contentHandler()->loadVersionInfo(
242
                $contentId,
243
                $versionNo
244
            );
245
        } catch (APINotFoundException $e) {
246
            throw new NotFoundException(
247
                'VersionInfo',
248
                [
249
                    'contentId' => $contentId,
250
                    'versionNo' => $versionNo,
251
                ],
252
                $e
253
            );
254
        }
255
256
        $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
257
258
        if ($versionInfo->isPublished()) {
259
            $function = 'read';
260
        } else {
261
            $function = 'versionread';
262
        }
263
264
        if (!$this->repository->canUser('content', $function, $versionInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
265
            throw new UnauthorizedException('content', $function, ['contentId' => $contentId]);
266
        }
267
268
        return $versionInfo;
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
275
    {
276
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
277
        if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
278
            $useAlwaysAvailable = false;
279
        }
280
281
        return $this->loadContent(
282
            $contentInfo->id,
283
            $languages,
284
            $versionNo,// On purpose pass as-is and not use $contentInfo, to make sure to return actual current version on null
285
            $useAlwaysAvailable
286
        );
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292
    public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages = null, $useAlwaysAvailable = true)
293
    {
294
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
295
        if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
296
            $useAlwaysAvailable = false;
297
        }
298
299
        return $this->loadContent(
300
            $versionInfo->getContentInfo()->id,
301
            $languages,
302
            $versionInfo->versionNo,
303
            $useAlwaysAvailable
304
        );
305
    }
306
307
    /**
308
     * {@inheritdoc}
309
     */
310
    public function loadContent($contentId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
311
    {
312
        $content = $this->internalLoadContent($contentId, $languages, $versionNo, false, $useAlwaysAvailable);
313
314
        if (!$this->repository->canUser('content', 'read', $content)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
315
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
316
        }
317
        if (
318
            !$content->getVersionInfo()->isPublished()
319
            && !$this->repository->canUser('content', 'versionread', $content)
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
320
        ) {
321
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId, 'versionNo' => $versionNo]);
322
        }
323
324
        return $content;
325
    }
326
327
    /**
328
     * Loads content in a version of the given content object.
329
     *
330
     * If no version number is given, the method returns the current version
331
     *
332
     * @internal
333
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the content or version with the given id and languages does not exist
334
     *
335
     * @param mixed $id
336
     * @param array|null $languages A language priority, filters returned fields and is used as prioritized language code on
337
     *                         returned value object. If not given all languages are returned.
338
     * @param int|null $versionNo the version number. If not given the current version is returned
339
     * @param bool $isRemoteId
340
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
341
     *
342
     * @return \eZ\Publish\API\Repository\Values\Content\Content
343
     */
344
    public function internalLoadContent($id, array $languages = null, $versionNo = null, $isRemoteId = false, $useAlwaysAvailable = true)
345
    {
346
        try {
347
            // Get Content ID if lookup by remote ID
348
            if ($isRemoteId) {
349
                $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($id);
350
                $id = $spiContentInfo->id;
351
                // Set $isRemoteId to false as the next loads will be for content id now that we have it (for exception use now)
352
                $isRemoteId = false;
353
            }
354
355
            $loadLanguages = $languages;
356
            $alwaysAvailableLanguageCode = null;
357
            // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
358
            // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
359
            if (!empty($loadLanguages) && $useAlwaysAvailable) {
360
                if (!isset($spiContentInfo)) {
361
                    $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
362
                }
363
364
                if ($spiContentInfo->alwaysAvailable) {
365
                    $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
366
                    $loadLanguages = array_unique($loadLanguages);
367
                }
368
            }
369
370
            $spiContent = $this->persistenceHandler->contentHandler()->load(
371
                $id,
372
                $versionNo,
373
                $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 355 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...
374
            );
375
        } catch (APINotFoundException $e) {
376
            throw new NotFoundException(
377
                'Content',
378
                [
379
                    $isRemoteId ? 'remoteId' : 'id' => $id,
380
                    'languages' => $languages,
381
                    'versionNo' => $versionNo,
382
                ],
383
                $e
384
            );
385
        }
386
387
        return $this->domainMapper->buildContentDomainObject(
388
            $spiContent,
389
            $this->repository->getContentTypeService()->loadContentType(
390
                $spiContent->versionInfo->contentInfo->contentTypeId
391
            ),
392
            $languages ?? [],
393
            $alwaysAvailableLanguageCode
394
        );
395
    }
396
397
    /**
398
     * Loads content in a version for the content object reference by the given remote id.
399
     *
400
     * If no version is given, the method returns the current version
401
     *
402
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content or version with the given remote id does not exist
403
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the user has no access to read content and in case of un-published content: read versions
404
     *
405
     * @param string $remoteId
406
     * @param array $languages A language filter for fields. If not given all languages are returned
407
     * @param int $versionNo the version number. If not given the current version is returned
408
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
409
     *
410
     * @return \eZ\Publish\API\Repository\Values\Content\Content
411
     */
412
    public function loadContentByRemoteId($remoteId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
413
    {
414
        $content = $this->internalLoadContent($remoteId, $languages, $versionNo, true, $useAlwaysAvailable);
415
416
        if (!$this->repository->canUser('content', 'read', $content)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
417
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
418
        }
419
420
        if (
421
            !$content->getVersionInfo()->isPublished()
422
            && !$this->repository->canUser('content', 'versionread', $content)
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
423
        ) {
424
            throw new UnauthorizedException('content', 'versionread', ['remoteId' => $remoteId, 'versionNo' => $versionNo]);
425
        }
426
427
        return $content;
428
    }
429
430
    /**
431
     * Bulk-load Content items by the list of ContentInfo Value Objects.
432
     *
433
     * Note: it does not throw exceptions on load, just ignores erroneous Content item.
434
     * Moreover, since the method works on pre-loaded ContentInfo list, it is assumed that user is
435
     * allowed to access every Content on the list.
436
     *
437
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo[] $contentInfoList
438
     * @param string[] $languages A language priority, filters returned fields and is used as prioritized language code on
439
     *                            returned value object. If not given all languages are returned.
440
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true,
441
     *                                 unless all languages have been asked for.
442
     *
443
     * @return \eZ\Publish\API\Repository\Values\Content\Content[] list of Content items with Content Ids as keys
444
     */
445
    public function loadContentListByContentInfo(
446
        array $contentInfoList,
447
        array $languages = [],
448
        $useAlwaysAvailable = true
449
    ) {
450
        $loadAllLanguages = $languages === Language::ALL;
451
        $contentIds = [];
452
        $contentTypeIds = [];
453
        $translations = $languages;
454
        foreach ($contentInfoList as $contentInfo) {
455
            $contentIds[] = $contentInfo->id;
456
            $contentTypeIds[] = $contentInfo->contentTypeId;
457
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
458
            // Might in some case load more languages then intended, but prioritised handling will pick right one
459
            if (!$loadAllLanguages && $useAlwaysAvailable && $contentInfo->alwaysAvailable) {
460
                $translations[] = $contentInfo->mainLanguageCode;
461
            }
462
        }
463
464
        $contentList = [];
465
        $translations = array_unique($translations);
466
        $spiContentList = $this->persistenceHandler->contentHandler()->loadContentList(
467
            $contentIds,
468
            $translations
469
        );
470
        $contentTypeList = $this->repository->getContentTypeService()->loadContentTypeList(
471
            array_unique($contentTypeIds),
472
            $languages
473
        );
474
        foreach ($spiContentList as $contentId => $spiContent) {
475
            $contentInfo = $spiContent->versionInfo->contentInfo;
476
            $contentList[$contentId] = $this->domainMapper->buildContentDomainObject(
477
                $spiContent,
478
                $contentTypeList[$contentInfo->contentTypeId],
479
                $languages,
480
                $contentInfo->alwaysAvailable ? $contentInfo->mainLanguageCode : null
481
            );
482
        }
483
484
        return $contentList;
485
    }
486
487
    /**
488
     * Creates a new content draft assigned to the authenticated user.
489
     *
490
     * If a different userId is given in $contentCreateStruct it is assigned to the given user
491
     * but this required special rights for the authenticated user
492
     * (this is useful for content staging where the transfer process does not
493
     * have to authenticate with the user which created the content object in the source server).
494
     * The user has to publish the draft if it should be visible.
495
     * In 4.x at least one location has to be provided in the location creation array.
496
     *
497
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to create the content in the given location
498
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the provided remoteId exists in the system, required properties on
499
     *                                                                        struct are missing or invalid, or if multiple locations are under the
500
     *                                                                        same parent.
501
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
502
     *                                                                               or if a required field is missing / set to an empty value.
503
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
504
     *                                                                          or value is set for non-translatable field in language
505
     *                                                                          other than main.
506
     *
507
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
508
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs For each location parent under which a location should be created for the content
509
     *
510
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
511
     */
512
    public function createContent(APIContentCreateStruct $contentCreateStruct, array $locationCreateStructs = [])
513
    {
514
        if ($contentCreateStruct->mainLanguageCode === null) {
515
            throw new InvalidArgumentException('$contentCreateStruct', "'mainLanguageCode' property must be set");
516
        }
517
518
        if ($contentCreateStruct->contentType === null) {
519
            throw new InvalidArgumentException('$contentCreateStruct', "'contentType' property must be set");
520
        }
521
522
        $contentCreateStruct = clone $contentCreateStruct;
523
524
        if ($contentCreateStruct->ownerId === null) {
525
            $contentCreateStruct->ownerId = $this->repository->getCurrentUserReference()->getUserId();
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Reposito...tCurrentUserReference() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::getCurrentUserReference() instead. Get current user reference.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
526
        }
527
528
        if ($contentCreateStruct->alwaysAvailable === null) {
529
            $contentCreateStruct->alwaysAvailable = $contentCreateStruct->contentType->defaultAlwaysAvailable ?: false;
530
        }
531
532
        $contentCreateStruct->contentType = $this->repository->getContentTypeService()->loadContentType(
533
            $contentCreateStruct->contentType->id
534
        );
535
536
        if (empty($contentCreateStruct->sectionId)) {
537
            if (isset($locationCreateStructs[0])) {
538
                $location = $this->repository->getLocationService()->loadLocation(
539
                    $locationCreateStructs[0]->parentLocationId
540
                );
541
                $contentCreateStruct->sectionId = $location->contentInfo->sectionId;
542
            } else {
543
                $contentCreateStruct->sectionId = 1;
544
            }
545
        }
546
547
        if (!$this->repository->canUser('content', 'create', $contentCreateStruct, $locationCreateStructs)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
548
            throw new UnauthorizedException(
549
                'content',
550
                'create',
551
                [
552
                    'parentLocationId' => isset($locationCreateStructs[0]) ?
553
                            $locationCreateStructs[0]->parentLocationId :
554
                            null,
555
                    'sectionId' => $contentCreateStruct->sectionId,
556
                ]
557
            );
558
        }
559
560
        if (!empty($contentCreateStruct->remoteId)) {
561
            try {
562
                $this->loadContentByRemoteId($contentCreateStruct->remoteId);
563
564
                throw new InvalidArgumentException(
565
                    '$contentCreateStruct',
566
                    "Another content with remoteId '{$contentCreateStruct->remoteId}' exists"
567
                );
568
            } catch (APINotFoundException $e) {
569
                // Do nothing
570
            }
571
        } else {
572
            $contentCreateStruct->remoteId = $this->domainMapper->getUniqueHash($contentCreateStruct);
573
        }
574
575
        $spiLocationCreateStructs = $this->buildSPILocationCreateStructs($locationCreateStructs);
576
577
        $languageCodes = $this->getLanguageCodesForCreate($contentCreateStruct);
578
        $fields = $this->mapFieldsForCreate($contentCreateStruct);
579
580
        $fieldValues = [];
581
        $spiFields = [];
582
        $allFieldErrors = [];
583
        $inputRelations = [];
584
        $locationIdToContentIdMapping = [];
585
586
        foreach ($contentCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
587
            /** @var $fieldType \eZ\Publish\Core\FieldType\FieldType */
588
            $fieldType = $this->fieldTypeRegistry->getFieldType(
589
                $fieldDefinition->fieldTypeIdentifier
590
            );
591
592
            foreach ($languageCodes as $languageCode) {
593
                $isEmptyValue = false;
594
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $contentCreateStruct->mainLanguageCode;
595
                $isLanguageMain = $languageCode === $contentCreateStruct->mainLanguageCode;
596
                if (isset($fields[$fieldDefinition->identifier][$valueLanguageCode])) {
597
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
598
                } else {
599
                    $fieldValue = $fieldDefinition->defaultValue;
600
                }
601
602
                $fieldValue = $fieldType->acceptValue($fieldValue);
603
604
                if ($fieldType->isEmptyValue($fieldValue)) {
605
                    $isEmptyValue = true;
606
                    if ($fieldDefinition->isRequired) {
607
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
608
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
609
                            null,
610
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
611
                            'empty'
612
                        );
613
                    }
614
                } else {
615
                    $fieldErrors = $fieldType->validate(
616
                        $fieldDefinition,
617
                        $fieldValue
618
                    );
619
                    if (!empty($fieldErrors)) {
620
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
621
                    }
622
                }
623
624
                if (!empty($allFieldErrors)) {
625
                    continue;
626
                }
627
628
                $this->relationProcessor->appendFieldRelations(
629
                    $inputRelations,
630
                    $locationIdToContentIdMapping,
631
                    $fieldType,
632
                    $fieldValue,
633
                    $fieldDefinition->id
634
                );
635
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
636
637
                // Only non-empty value for: translatable field or in main language
638
                if (
639
                    (!$isEmptyValue && $fieldDefinition->isTranslatable) ||
640
                    (!$isEmptyValue && $isLanguageMain)
641
                ) {
642
                    $spiFields[] = new SPIField(
643
                        [
644
                            'id' => null,
645
                            'fieldDefinitionId' => $fieldDefinition->id,
646
                            'type' => $fieldDefinition->fieldTypeIdentifier,
647
                            'value' => $fieldType->toPersistenceValue($fieldValue),
648
                            'languageCode' => $languageCode,
649
                            'versionNo' => null,
650
                        ]
651
                    );
652
                }
653
            }
654
        }
655
656
        if (!empty($allFieldErrors)) {
657
            throw new ContentFieldValidationException($allFieldErrors);
658
        }
659
660
        $spiContentCreateStruct = new SPIContentCreateStruct(
661
            [
662
                'name' => $this->nameSchemaService->resolve(
663
                    $contentCreateStruct->contentType->nameSchema,
664
                    $contentCreateStruct->contentType,
665
                    $fieldValues,
666
                    $languageCodes
667
                ),
668
                'typeId' => $contentCreateStruct->contentType->id,
669
                'sectionId' => $contentCreateStruct->sectionId,
670
                'ownerId' => $contentCreateStruct->ownerId,
671
                'locations' => $spiLocationCreateStructs,
672
                'fields' => $spiFields,
673
                'alwaysAvailable' => $contentCreateStruct->alwaysAvailable,
674
                'remoteId' => $contentCreateStruct->remoteId,
675
                'modified' => isset($contentCreateStruct->modificationDate) ? $contentCreateStruct->modificationDate->getTimestamp() : time(),
676
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
677
                    $contentCreateStruct->mainLanguageCode
678
                )->id,
679
            ]
680
        );
681
682
        $defaultObjectStates = $this->getDefaultObjectStates();
683
684
        $this->repository->beginTransaction();
685
        try {
686
            $spiContent = $this->persistenceHandler->contentHandler()->create($spiContentCreateStruct);
687
            $this->relationProcessor->processFieldRelations(
688
                $inputRelations,
689
                $spiContent->versionInfo->contentInfo->id,
690
                $spiContent->versionInfo->versionNo,
691
                $contentCreateStruct->contentType
692
            );
693
694
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
695
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
696
                $objectStateHandler->setContentState(
697
                    $spiContent->versionInfo->contentInfo->id,
698
                    $objectStateGroupId,
699
                    $objectState->id
700
                );
701
            }
702
703
            $this->repository->commit();
704
        } catch (Exception $e) {
705
            $this->repository->rollback();
706
            throw $e;
707
        }
708
709
        return $this->domainMapper->buildContentDomainObject(
710
            $spiContent,
711
            $contentCreateStruct->contentType
712
        );
713
    }
714
715
    /**
716
     * Returns an array of default content states with content state group id as key.
717
     *
718
     * @return \eZ\Publish\SPI\Persistence\Content\ObjectState[]
719
     */
720
    protected function getDefaultObjectStates()
721
    {
722
        $defaultObjectStatesMap = [];
723
        $objectStateHandler = $this->persistenceHandler->objectStateHandler();
724
725
        foreach ($objectStateHandler->loadAllGroups() as $objectStateGroup) {
726
            foreach ($objectStateHandler->loadObjectStates($objectStateGroup->id) as $objectState) {
727
                // Only register the first object state which is the default one.
728
                $defaultObjectStatesMap[$objectStateGroup->id] = $objectState;
729
                break;
730
            }
731
        }
732
733
        return $defaultObjectStatesMap;
734
    }
735
736
    /**
737
     * Returns all language codes used in given $fields.
738
     *
739
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value is set in main language
740
     *
741
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
742
     *
743
     * @return string[]
744
     */
745
    protected function getLanguageCodesForCreate(APIContentCreateStruct $contentCreateStruct)
746
    {
747
        $languageCodes = [];
748
749
        foreach ($contentCreateStruct->fields as $field) {
750
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
751
                continue;
752
            }
753
754
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
755
                $field->languageCode
756
            );
757
            $languageCodes[$field->languageCode] = true;
758
        }
759
760
        if (!isset($languageCodes[$contentCreateStruct->mainLanguageCode])) {
761
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
762
                $contentCreateStruct->mainLanguageCode
763
            );
764
            $languageCodes[$contentCreateStruct->mainLanguageCode] = true;
765
        }
766
767
        return array_keys($languageCodes);
768
    }
769
770
    /**
771
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
772
     *
773
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
774
     *                                                                          or value is set for non-translatable field in language
775
     *                                                                          other than main
776
     *
777
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
778
     *
779
     * @return array
780
     */
781
    protected function mapFieldsForCreate(APIContentCreateStruct $contentCreateStruct)
782
    {
783
        $fields = [];
784
785
        foreach ($contentCreateStruct->fields as $field) {
786
            $fieldDefinition = $contentCreateStruct->contentType->getFieldDefinition($field->fieldDefIdentifier);
787
788
            if ($fieldDefinition === null) {
789
                throw new ContentValidationException(
790
                    "Field definition '%identifier%' does not exist in given ContentType",
791
                    ['%identifier%' => $field->fieldDefIdentifier]
792
                );
793
            }
794
795
            if ($field->languageCode === null) {
796
                $field = $this->cloneField(
797
                    $field,
798
                    ['languageCode' => $contentCreateStruct->mainLanguageCode]
799
                );
800
            }
801
802
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $contentCreateStruct->mainLanguageCode)) {
803
                throw new ContentValidationException(
804
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
805
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
806
                );
807
            }
808
809
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
810
        }
811
812
        return $fields;
813
    }
814
815
    /**
816
     * Clones $field with overriding specific properties from given $overrides array.
817
     *
818
     * @param Field $field
819
     * @param array $overrides
820
     *
821
     * @return Field
822
     */
823
    private function cloneField(Field $field, array $overrides = [])
824
    {
825
        $fieldData = array_merge(
826
            [
827
                'id' => $field->id,
828
                'value' => $field->value,
829
                'languageCode' => $field->languageCode,
830
                'fieldDefIdentifier' => $field->fieldDefIdentifier,
831
                'fieldTypeIdentifier' => $field->fieldTypeIdentifier,
832
            ],
833
            $overrides
834
        );
835
836
        return new Field($fieldData);
837
    }
838
839
    /**
840
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
841
     *
842
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs
843
     *
844
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct[]
845
     */
846
    protected function buildSPILocationCreateStructs(array $locationCreateStructs)
847
    {
848
        $spiLocationCreateStructs = [];
849
        $parentLocationIdSet = [];
850
        $mainLocation = true;
851
852
        foreach ($locationCreateStructs as $locationCreateStruct) {
853
            if (isset($parentLocationIdSet[$locationCreateStruct->parentLocationId])) {
854
                throw new InvalidArgumentException(
855
                    '$locationCreateStructs',
856
                    "Multiple LocationCreateStructs with the same parent Location '{$locationCreateStruct->parentLocationId}' are given"
857
                );
858
            }
859
860
            if (!array_key_exists($locationCreateStruct->sortField, Location::SORT_FIELD_MAP)) {
861
                $locationCreateStruct->sortField = Location::SORT_FIELD_NAME;
862
            }
863
864
            if (!array_key_exists($locationCreateStruct->sortOrder, Location::SORT_ORDER_MAP)) {
865
                $locationCreateStruct->sortOrder = Location::SORT_ORDER_ASC;
866
            }
867
868
            $parentLocationIdSet[$locationCreateStruct->parentLocationId] = true;
869
            $parentLocation = $this->repository->getLocationService()->loadLocation(
870
                $locationCreateStruct->parentLocationId
871
            );
872
873
            $spiLocationCreateStructs[] = $this->domainMapper->buildSPILocationCreateStruct(
874
                $locationCreateStruct,
875
                $parentLocation,
876
                $mainLocation,
877
                // For Content draft contentId and contentVersionNo are set in ContentHandler upon draft creation
878
                null,
879
                null
880
            );
881
882
            // First Location in the list will be created as main Location
883
            $mainLocation = false;
884
        }
885
886
        return $spiLocationCreateStructs;
887
    }
888
889
    /**
890
     * Updates the metadata.
891
     *
892
     * (see {@link ContentMetadataUpdateStruct}) of a content object - to update fields use updateContent
893
     *
894
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update the content meta data
895
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the remoteId in $contentMetadataUpdateStruct is set but already exists
896
     *
897
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
898
     * @param \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct $contentMetadataUpdateStruct
899
     *
900
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content with the updated attributes
901
     */
902
    public function updateContentMetadata(ContentInfo $contentInfo, ContentMetadataUpdateStruct $contentMetadataUpdateStruct)
903
    {
904
        $propertyCount = 0;
905
        foreach ($contentMetadataUpdateStruct as $propertyName => $propertyValue) {
0 ignored issues
show
Bug introduced by
The expression $contentMetadataUpdateStruct of type object<eZ\Publish\API\Re...ntMetadataUpdateStruct> is not traversable.
Loading history...
906
            if (isset($contentMetadataUpdateStruct->$propertyName)) {
907
                $propertyCount += 1;
908
            }
909
        }
910
        if ($propertyCount === 0) {
911
            throw new InvalidArgumentException(
912
                '$contentMetadataUpdateStruct',
913
                'At least one property must be set'
914
            );
915
        }
916
917
        $loadedContentInfo = $this->loadContentInfo($contentInfo->id);
918
919
        if (!$this->repository->canUser('content', 'edit', $loadedContentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
920
            throw new UnauthorizedException('content', 'edit', ['contentId' => $loadedContentInfo->id]);
921
        }
922
923
        if (isset($contentMetadataUpdateStruct->remoteId)) {
924
            try {
925
                $existingContentInfo = $this->loadContentInfoByRemoteId($contentMetadataUpdateStruct->remoteId);
926
927
                if ($existingContentInfo->id !== $loadedContentInfo->id) {
928
                    throw new InvalidArgumentException(
929
                        '$contentMetadataUpdateStruct',
930
                        "Another content with remoteId '{$contentMetadataUpdateStruct->remoteId}' exists"
931
                    );
932
                }
933
            } catch (APINotFoundException $e) {
934
                // Do nothing
935
            }
936
        }
937
938
        $this->repository->beginTransaction();
939
        try {
940
            if ($propertyCount > 1 || !isset($contentMetadataUpdateStruct->mainLocationId)) {
941
                $this->persistenceHandler->contentHandler()->updateMetadata(
942
                    $loadedContentInfo->id,
943
                    new SPIMetadataUpdateStruct(
944
                        [
945
                            'ownerId' => $contentMetadataUpdateStruct->ownerId,
946
                            'publicationDate' => isset($contentMetadataUpdateStruct->publishedDate) ?
947
                                $contentMetadataUpdateStruct->publishedDate->getTimestamp() :
948
                                null,
949
                            'modificationDate' => isset($contentMetadataUpdateStruct->modificationDate) ?
950
                                $contentMetadataUpdateStruct->modificationDate->getTimestamp() :
951
                                null,
952
                            'mainLanguageId' => isset($contentMetadataUpdateStruct->mainLanguageCode) ?
953
                                $this->repository->getContentLanguageService()->loadLanguage(
954
                                    $contentMetadataUpdateStruct->mainLanguageCode
955
                                )->id :
956
                                null,
957
                            'alwaysAvailable' => $contentMetadataUpdateStruct->alwaysAvailable,
958
                            'remoteId' => $contentMetadataUpdateStruct->remoteId,
959
                            'name' => $contentMetadataUpdateStruct->name,
960
                        ]
961
                    )
962
                );
963
            }
964
965
            // Change main location
966
            if (isset($contentMetadataUpdateStruct->mainLocationId)
967
                && $loadedContentInfo->mainLocationId !== $contentMetadataUpdateStruct->mainLocationId) {
968
                $this->persistenceHandler->locationHandler()->changeMainLocation(
969
                    $loadedContentInfo->id,
970
                    $contentMetadataUpdateStruct->mainLocationId
971
                );
972
            }
973
974
            // Republish URL aliases to update always-available flag
975
            if (isset($contentMetadataUpdateStruct->alwaysAvailable)
976
                && $loadedContentInfo->alwaysAvailable !== $contentMetadataUpdateStruct->alwaysAvailable) {
977
                $content = $this->loadContent($loadedContentInfo->id);
978
                $this->publishUrlAliasesForContent($content, false);
979
            }
980
981
            $this->repository->commit();
982
        } catch (Exception $e) {
983
            $this->repository->rollback();
984
            throw $e;
985
        }
986
987
        return isset($content) ? $content : $this->loadContent($loadedContentInfo->id);
988
    }
989
990
    /**
991
     * Publishes URL aliases for all locations of a given content.
992
     *
993
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
994
     * @param bool $updatePathIdentificationString this parameter is legacy storage specific for updating
995
     *                      ezcontentobject_tree.path_identification_string, it is ignored by other storage engines
996
     */
997
    protected function publishUrlAliasesForContent(APIContent $content, $updatePathIdentificationString = true)
998
    {
999
        $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
1000
        $locations = $this->repository->getLocationService()->loadLocations(
1001
            $content->getVersionInfo()->getContentInfo()
1002
        );
1003
        $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
1004
        foreach ($locations as $location) {
1005
            foreach ($urlAliasNames as $languageCode => $name) {
1006
                $urlAliasHandler->publishUrlAliasForLocation(
1007
                    $location->id,
1008
                    $location->parentLocationId,
1009
                    $name,
1010
                    $languageCode,
1011
                    $content->contentInfo->alwaysAvailable,
1012
                    $updatePathIdentificationString ? $languageCode === $content->contentInfo->mainLanguageCode : false
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $languageCode (integer) and $content->contentInfo->mainLanguageCode (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
1013
                );
1014
            }
1015
            // archive URL aliases of Translations that got deleted
1016
            $urlAliasHandler->archiveUrlAliasesForDeletedTranslations(
1017
                $location->id,
1018
                $location->parentLocationId,
1019
                $content->versionInfo->languageCodes
1020
            );
1021
        }
1022
    }
1023
1024
    /**
1025
     * Deletes a content object including all its versions and locations including their subtrees.
1026
     *
1027
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to delete the content (in one of the locations of the given content object)
1028
     *
1029
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1030
     *
1031
     * @return mixed[] Affected Location Id's
1032
     */
1033
    public function deleteContent(ContentInfo $contentInfo)
1034
    {
1035
        $contentInfo = $this->internalLoadContentInfo($contentInfo->id);
1036
1037
        if (!$this->repository->canUser('content', 'remove', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1038
            throw new UnauthorizedException('content', 'remove', ['contentId' => $contentInfo->id]);
1039
        }
1040
1041
        $affectedLocations = [];
1042
        $this->repository->beginTransaction();
1043
        try {
1044
            // Load Locations first as deleting Content also deletes belonging Locations
1045
            $spiLocations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentInfo->id);
1046
            $this->persistenceHandler->contentHandler()->deleteContent($contentInfo->id);
1047
            $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
1048
            foreach ($spiLocations as $spiLocation) {
1049
                $urlAliasHandler->locationDeleted($spiLocation->id);
1050
                $affectedLocations[] = $spiLocation->id;
1051
            }
1052
            $this->repository->commit();
1053
        } catch (Exception $e) {
1054
            $this->repository->rollback();
1055
            throw $e;
1056
        }
1057
1058
        return $affectedLocations;
1059
    }
1060
1061
    /**
1062
     * Creates a draft from a published or archived version.
1063
     *
1064
     * If no version is given, the current published version is used.
1065
     * 4.x: The draft is created with the initialLanguage code of the source version or if not present with the main language.
1066
     * It can be changed on updating the version.
1067
     *
1068
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1069
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1070
     * @param \eZ\Publish\API\Repository\Values\User\User $creator if set given user is used to create the draft - otherwise the current-user is used
1071
     *
1072
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
1073
     *
1074
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
1075
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the current-user is not allowed to create the draft
1076
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to create the draft
1077
     */
1078
    public function createContentDraft(ContentInfo $contentInfo, APIVersionInfo $versionInfo = null, User $creator = null)
1079
    {
1080
        $contentInfo = $this->loadContentInfo($contentInfo->id);
1081
1082
        if ($versionInfo !== null) {
1083
            // Check that given $contentInfo and $versionInfo belong to the same content
1084
            if ($versionInfo->getContentInfo()->id != $contentInfo->id) {
1085
                throw new InvalidArgumentException(
1086
                    '$versionInfo',
1087
                    'VersionInfo does not belong to the same content as given ContentInfo'
1088
                );
1089
            }
1090
1091
            $versionInfo = $this->loadVersionInfoById($contentInfo->id, $versionInfo->versionNo);
1092
1093
            switch ($versionInfo->status) {
1094
                case VersionInfo::STATUS_PUBLISHED:
1095
                case VersionInfo::STATUS_ARCHIVED:
1096
                    break;
1097
1098
                default:
1099
                    // @todo: throw an exception here, to be defined
1100
                    throw new BadStateException(
1101
                        '$versionInfo',
1102
                        'Draft can not be created from a draft version'
1103
                    );
1104
            }
1105
1106
            $versionNo = $versionInfo->versionNo;
1107
        } elseif ($contentInfo->published) {
1108
            $versionNo = $contentInfo->currentVersionNo;
1109
        } else {
1110
            // @todo: throw an exception here, to be defined
1111
            throw new BadStateException(
1112
                '$contentInfo',
1113
                'Content is not published, draft can be created only from published or archived version'
1114
            );
1115
        }
1116
1117
        if ($creator === null) {
1118
            $creator = $this->repository->getCurrentUserReference();
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Reposito...tCurrentUserReference() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::getCurrentUserReference() instead. Get current user reference.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1119
        }
1120
1121
        if (!$this->repository->getPermissionResolver()->canUser(
1122
            'content',
1123
            'edit',
1124
            $contentInfo,
1125
            [
1126
                (new Target\Builder\VersionBuilder())
1127
                    ->changeStatusTo(APIVersionInfo::STATUS_DRAFT)
1128
                    ->build(),
1129
            ]
1130
        )) {
1131
            throw new UnauthorizedException(
1132
                'content',
1133
                'edit',
1134
                ['contentId' => $contentInfo->id]
1135
            );
1136
        }
1137
1138
        $this->repository->beginTransaction();
1139
        try {
1140
            $spiContent = $this->persistenceHandler->contentHandler()->createDraftFromVersion(
1141
                $contentInfo->id,
1142
                $versionNo,
1143
                $creator->getUserId()
1144
            );
1145
            $this->repository->commit();
1146
        } catch (Exception $e) {
1147
            $this->repository->rollback();
1148
            throw $e;
1149
        }
1150
1151
        return $this->domainMapper->buildContentDomainObject(
1152
            $spiContent,
1153
            $this->repository->getContentTypeService()->loadContentType(
1154
                $spiContent->versionInfo->contentInfo->contentTypeId
1155
            )
1156
        );
1157
    }
1158
1159
    /**
1160
     * Counts drafts for a user.
1161
     *
1162
     * If no user is given the number of drafts for the authenticated user are returned
1163
     *
1164
     * @param \eZ\Publish\API\Repository\Values\User\User $user The user to load drafts for, if defined, otherwise drafts for current-user
1165
     *
1166
     * @return int The number of drafts ({@link VersionInfo}) owned by the given user
1167
     */
1168
    public function countContentDrafts(?User $user = null): int
1169
    {
1170
        return $this->persistenceHandler->contentHandler()->countDraftsForUser(
1171
            $this->resolveUser($user)->getUserId()
1172
        );
1173
    }
1174
1175
    /**
1176
     * Loads drafts for a user.
1177
     *
1178
     * If no user is given the drafts for the authenticated user are returned
1179
     *
1180
     * @param \eZ\Publish\API\Repository\Values\User\User $user
1181
     *
1182
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Drafts owned by the given user
1183
     *
1184
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1185
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1186
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1187
     */
1188
    public function loadContentDrafts(User $user = null)
1189
    {
1190
        // throw early if user has absolutely no access to versionread
1191
        if ($this->repository->hasAccess('content', 'versionread') === false) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::hasAccess() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::hasAccess() instead. Check if user has access to a given module / function. Low level function, use canUser instead if you have objects to check against.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1192
            throw new UnauthorizedException('content', 'versionread');
1193
        }
1194
1195
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftsForUser(
1196
            $this->resolveUser($user)->getUserId()
1197
        );
1198
        $versionInfoList = [];
1199
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1200
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1201
            // @todo: Change this to filter returned drafts by permissions instead of throwing
1202
            if (!$this->repository->canUser('content', 'versionread', $versionInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1203
                throw new UnauthorizedException('content', 'versionread', ['contentId' => $versionInfo->contentInfo->id]);
1204
            }
1205
1206
            $versionInfoList[] = $versionInfo;
1207
        }
1208
1209
        return $versionInfoList;
1210
    }
1211
1212
    /**
1213
     * {@inheritdoc}
1214
     */
1215
    public function loadContentDraftList(?User $user = null, int $offset = 0, int $limit = -1): ContentDraftList
1216
    {
1217
        $list = new ContentDraftList();
1218
        if ($this->repository->hasAccess('content', 'versionread') === false) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::hasAccess() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::hasAccess() instead. Check if user has access to a given module / function. Low level function, use canUser instead if you have objects to check against.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1219
            return $list;
1220
        }
1221
1222
        $list->totalCount = $this->persistenceHandler->contentHandler()->countDraftsForUser(
1223
            $this->resolveUser($user)->getUserId()
1224
        );
1225
        if ($list->totalCount > 0) {
1226
            $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftListForUser(
1227
                $this->resolveUser($user)->getUserId(),
1228
                $offset,
1229
                $limit
1230
            );
1231
            $contentDraftList = [];
1232
            foreach ($spiVersionInfoList as $spiVersionInfo) {
1233
                $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1234
                if ($this->repository->canUser('content', 'versionread', $versionInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1235
                    $contentDraftList[] = new ContentDraftListItem($versionInfo);
1236
                } else {
1237
                    $contentDraftList[] = new UnauthorizedContentDraftListItem(
1238
                        'content',
1239
                        'versionread',
1240
                        ['contentId' => $versionInfo->contentInfo->id]
1241
                    );
1242
                }
1243
            }
1244
1245
            $list->items = $contentDraftList;
1246
        }
1247
1248
        return $list;
1249
    }
1250
1251
    /**
1252
     * Updates the fields of a draft.
1253
     *
1254
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1255
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1256
     *
1257
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the updated fields
1258
     *
1259
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
1260
     *                                                                               or if a required field is missing / set to an empty value.
1261
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
1262
     *                                                                          or value is set for non-translatable field in language
1263
     *                                                                          other than main.
1264
     *
1265
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
1266
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1267
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
1268
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1269
     */
1270
    public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct)
1271
    {
1272
        $contentUpdateStruct = clone $contentUpdateStruct;
1273
1274
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1275
        $content = $this->loadContent(
1276
            $versionInfo->getContentInfo()->id,
1277
            null,
1278
            $versionInfo->versionNo
1279
        );
1280
        if (!$content->versionInfo->isDraft()) {
1281
            throw new BadStateException(
1282
                '$versionInfo',
1283
                'Version is not a draft and can not be updated'
1284
            );
1285
        }
1286
1287
        if (!$this->repository->getPermissionResolver()->canUser(
1288
            'content',
1289
            'edit',
1290
            $content,
1291
            [
1292
                (new Target\Builder\VersionBuilder())
1293
                    ->updateFieldsTo(
1294
                        $contentUpdateStruct->initialLanguageCode,
1295
                        $contentUpdateStruct->fields
1296
                    )
1297
                    ->build(),
1298
            ]
1299
        )) {
1300
            throw new UnauthorizedException('content', 'edit', ['contentId' => $content->id]);
1301
        }
1302
1303
        $mainLanguageCode = $content->contentInfo->mainLanguageCode;
1304
        if ($contentUpdateStruct->initialLanguageCode === null) {
1305
            $contentUpdateStruct->initialLanguageCode = $mainLanguageCode;
1306
        }
1307
1308
        $allLanguageCodes = $this->getLanguageCodesForUpdate($contentUpdateStruct, $content);
1309
        $contentLanguageHandler = $this->persistenceHandler->contentLanguageHandler();
1310
        foreach ($allLanguageCodes as $languageCode) {
1311
            $contentLanguageHandler->loadByLanguageCode($languageCode);
1312
        }
1313
1314
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1315
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1316
            $content->contentInfo->contentTypeId
1317
        );
1318
        $fields = $this->mapFieldsForUpdate(
1319
            $contentUpdateStruct,
1320
            $contentType,
1321
            $mainLanguageCode
1322
        );
1323
1324
        $fieldValues = [];
1325
        $spiFields = [];
1326
        $allFieldErrors = [];
1327
        $inputRelations = [];
1328
        $locationIdToContentIdMapping = [];
1329
1330
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1331
            /** @var $fieldType \eZ\Publish\SPI\FieldType\FieldType */
1332
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1333
                $fieldDefinition->fieldTypeIdentifier
1334
            );
1335
1336
            foreach ($allLanguageCodes as $languageCode) {
1337
                $isCopied = $isEmpty = $isRetained = false;
1338
                $isLanguageNew = !in_array($languageCode, $content->versionInfo->languageCodes);
1339
                $isLanguageUpdated = in_array($languageCode, $updatedLanguageCodes);
1340
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $mainLanguageCode;
1341
                $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
1342
                $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
1343
1344
                if (!$isFieldUpdated && !$isLanguageNew) {
1345
                    $isRetained = true;
1346
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1347
                } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
1348
                    $isCopied = true;
1349
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1350
                } elseif ($isFieldUpdated) {
1351
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
1352
                } else {
1353
                    $fieldValue = $fieldDefinition->defaultValue;
1354
                }
1355
1356
                $fieldValue = $fieldType->acceptValue($fieldValue);
1357
1358
                if ($fieldType->isEmptyValue($fieldValue)) {
1359
                    $isEmpty = true;
1360
                    if ($isLanguageUpdated && $fieldDefinition->isRequired) {
1361
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
1362
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
1363
                            null,
1364
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
1365
                            'empty'
1366
                        );
1367
                    }
1368
                } elseif ($isLanguageUpdated) {
1369
                    $fieldErrors = $fieldType->validate(
1370
                        $fieldDefinition,
1371
                        $fieldValue
1372
                    );
1373
                    if (!empty($fieldErrors)) {
1374
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
1375
                    }
1376
                }
1377
1378
                if (!empty($allFieldErrors)) {
1379
                    continue;
1380
                }
1381
1382
                $this->relationProcessor->appendFieldRelations(
1383
                    $inputRelations,
1384
                    $locationIdToContentIdMapping,
1385
                    $fieldType,
1386
                    $fieldValue,
0 ignored issues
show
Compatibility introduced by
$fieldValue of type object<eZ\Publish\SPI\FieldType\Value> is not a sub-type of object<eZ\Publish\Core\FieldType\Value>. It seems like you assume a concrete implementation of the interface eZ\Publish\SPI\FieldType\Value to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1387
                    $fieldDefinition->id
1388
                );
1389
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
1390
1391
                if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
1392
                    continue;
1393
                }
1394
1395
                $spiFields[] = new SPIField(
1396
                    [
1397
                        'id' => $isLanguageNew ?
1398
                            null :
1399
                            $content->getField($fieldDefinition->identifier, $languageCode)->id,
1400
                        'fieldDefinitionId' => $fieldDefinition->id,
1401
                        'type' => $fieldDefinition->fieldTypeIdentifier,
1402
                        'value' => $fieldType->toPersistenceValue($fieldValue),
1403
                        'languageCode' => $languageCode,
1404
                        'versionNo' => $versionInfo->versionNo,
1405
                    ]
1406
                );
1407
            }
1408
        }
1409
1410
        if (!empty($allFieldErrors)) {
1411
            throw new ContentFieldValidationException($allFieldErrors);
1412
        }
1413
1414
        $spiContentUpdateStruct = new SPIContentUpdateStruct(
1415
            [
1416
                'name' => $this->nameSchemaService->resolveNameSchema(
1417
                    $content,
1418
                    $fieldValues,
1419
                    $allLanguageCodes,
1420
                    $contentType
1421
                ),
1422
                'creatorId' => $contentUpdateStruct->creatorId ?: $this->repository->getCurrentUserReference()->getUserId(),
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Reposito...tCurrentUserReference() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::getCurrentUserReference() instead. Get current user reference.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1423
                'fields' => $spiFields,
1424
                'modificationDate' => time(),
1425
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1426
                    $contentUpdateStruct->initialLanguageCode
1427
                )->id,
1428
            ]
1429
        );
1430
        $existingRelations = $this->loadRelations($versionInfo);
1431
1432
        $this->repository->beginTransaction();
1433
        try {
1434
            $spiContent = $this->persistenceHandler->contentHandler()->updateContent(
1435
                $versionInfo->getContentInfo()->id,
1436
                $versionInfo->versionNo,
1437
                $spiContentUpdateStruct
1438
            );
1439
            $this->relationProcessor->processFieldRelations(
1440
                $inputRelations,
1441
                $spiContent->versionInfo->contentInfo->id,
1442
                $spiContent->versionInfo->versionNo,
1443
                $contentType,
1444
                $existingRelations
1445
            );
1446
            $this->repository->commit();
1447
        } catch (Exception $e) {
1448
            $this->repository->rollback();
1449
            throw $e;
1450
        }
1451
1452
        return $this->domainMapper->buildContentDomainObject(
1453
            $spiContent,
1454
            $contentType
1455
        );
1456
    }
1457
1458
    /**
1459
     * Returns only updated language codes.
1460
     *
1461
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1462
     *
1463
     * @return array
1464
     */
1465
    private function getUpdatedLanguageCodes(APIContentUpdateStruct $contentUpdateStruct)
1466
    {
1467
        $languageCodes = [
1468
            $contentUpdateStruct->initialLanguageCode => true,
1469
        ];
1470
1471
        foreach ($contentUpdateStruct->fields as $field) {
1472
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
1473
                continue;
1474
            }
1475
1476
            $languageCodes[$field->languageCode] = true;
1477
        }
1478
1479
        return array_keys($languageCodes);
1480
    }
1481
1482
    /**
1483
     * Returns all language codes used in given $fields.
1484
     *
1485
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value exists in initial language
1486
     *
1487
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1488
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1489
     *
1490
     * @return array
1491
     */
1492
    protected function getLanguageCodesForUpdate(APIContentUpdateStruct $contentUpdateStruct, APIContent $content)
1493
    {
1494
        $languageCodes = array_fill_keys($content->versionInfo->languageCodes, true);
1495
        $languageCodes[$contentUpdateStruct->initialLanguageCode] = true;
1496
1497
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1498
        foreach ($updatedLanguageCodes as $languageCode) {
1499
            $languageCodes[$languageCode] = true;
1500
        }
1501
1502
        return array_keys($languageCodes);
1503
    }
1504
1505
    /**
1506
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
1507
     *
1508
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
1509
     *                                                                          or value is set for non-translatable field in language
1510
     *                                                                          other than main
1511
     *
1512
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1513
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1514
     * @param string $mainLanguageCode
1515
     *
1516
     * @return array
1517
     */
1518
    protected function mapFieldsForUpdate(
1519
        APIContentUpdateStruct $contentUpdateStruct,
1520
        ContentType $contentType,
1521
        $mainLanguageCode
1522
    ) {
1523
        $fields = [];
1524
1525
        foreach ($contentUpdateStruct->fields as $field) {
1526
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1527
1528
            if ($fieldDefinition === null) {
1529
                throw new ContentValidationException(
1530
                    "Field definition '%identifier%' does not exist in given ContentType",
1531
                    ['%identifier%' => $field->fieldDefIdentifier]
1532
                );
1533
            }
1534
1535
            if ($field->languageCode === null) {
1536
                if ($fieldDefinition->isTranslatable) {
1537
                    $languageCode = $contentUpdateStruct->initialLanguageCode;
1538
                } else {
1539
                    $languageCode = $mainLanguageCode;
1540
                }
1541
                $field = $this->cloneField($field, ['languageCode' => $languageCode]);
1542
            }
1543
1544
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $mainLanguageCode)) {
1545
                throw new ContentValidationException(
1546
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
1547
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
1548
                );
1549
            }
1550
1551
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
1552
        }
1553
1554
        return $fields;
1555
    }
1556
1557
    /**
1558
     * Publishes a content version.
1559
     *
1560
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1561
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1562
     *
1563
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1564
     * @param string[] $translations
1565
     *
1566
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1567
     *
1568
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1569
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1570
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1571
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1572
     */
1573
    public function publishVersion(APIVersionInfo $versionInfo, array $translations = Language::ALL)
1574
    {
1575
        $content = $this->internalLoadContent(
1576
            $versionInfo->contentInfo->id,
1577
            null,
1578
            $versionInfo->versionNo
1579
        );
1580
1581
        $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...
1582
        if ($content->contentInfo->currentVersionNo !== $versionInfo->versionNo) {
1583
            $fromContent = $this->internalLoadContent(
1584
                $content->contentInfo->id,
1585
                null,
1586
                $content->contentInfo->currentVersionNo
1587
            );
1588
            // should not occur now, might occur in case of un-publish
1589
            if (!$fromContent->contentInfo->isPublished()) {
1590
                $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...
1591
            }
1592
        }
1593
1594
        if (!$this->repository->getPermissionResolver()->canUser(
1595
            'content',
1596
            'publish',
1597
            $content
1598
        )) {
1599
            throw new UnauthorizedException(
1600
                'content', 'publish', ['contentId' => $content->id]
1601
            );
1602
        }
1603
1604
        $this->repository->beginTransaction();
1605
        try {
1606
            $this->copyTranslationsFromPublishedVersion($content->versionInfo, $translations);
1607
            $content = $this->internalPublishVersion($content->getVersionInfo(), null);
1608
            $this->repository->commit();
1609
        } catch (Exception $e) {
1610
            $this->repository->rollback();
1611
            throw $e;
1612
        }
1613
1614
        return $content;
1615
    }
1616
1617
    /**
1618
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1619
     * @param array $translations
1620
     *
1621
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1622
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException
1623
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
1624
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1625
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1626
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1627
     */
1628
    protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionInfo, array $translations = []): void
1629
    {
1630
        $contendId = $versionInfo->contentInfo->id;
1631
1632
        $currentContent = $this->internalLoadContent($contendId);
1633
        $currentVersionInfo = $currentContent->versionInfo;
1634
1635
        // Copying occurs only if:
1636
        // - There is published Version
1637
        // - Published version is older than the currently published one unless specific translations are provided.
1638
        if (!$currentVersionInfo->isPublished() ||
1639
            ($versionInfo->versionNo >= $currentVersionInfo->versionNo && empty($translations))) {
1640
            return;
1641
        }
1642
1643
        if (empty($translations)) {
1644
            $languagesToCopy = array_diff(
1645
                $currentVersionInfo->languageCodes,
1646
                $versionInfo->languageCodes
1647
            );
1648
        } else {
1649
            $languagesToCopy = array_diff(
1650
                $currentVersionInfo->languageCodes,
1651
                $translations
1652
            );
1653
        }
1654
1655
        if (empty($languagesToCopy)) {
1656
            return;
1657
        }
1658
1659
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1660
            $currentVersionInfo->contentInfo->contentTypeId
1661
        );
1662
1663
        // Find only translatable fields to update with selected languages
1664
        $updateStruct = $this->newContentUpdateStruct();
1665
        $updateStruct->initialLanguageCode = $versionInfo->initialLanguageCode;
1666
1667
        foreach ($currentContent->getFields() as $field) {
1668
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1669
1670
            if ($fieldDefinition->isTranslatable && in_array($field->languageCode, $languagesToCopy)) {
1671
                $updateStruct->setField($field->fieldDefIdentifier, $field->value, $field->languageCode);
1672
            }
1673
        }
1674
1675
        $this->updateContent($versionInfo, $updateStruct);
1676
    }
1677
1678
    /**
1679
     * Publishes a content version.
1680
     *
1681
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1682
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1683
     *
1684
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1685
     *
1686
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1687
     * @param int|null $publicationDate If null existing date is kept if there is one, otherwise current time is used.
1688
     *
1689
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1690
     */
1691
    protected function internalPublishVersion(APIVersionInfo $versionInfo, $publicationDate = null)
1692
    {
1693
        if (!$versionInfo->isDraft()) {
1694
            throw new BadStateException('$versionInfo', 'Only versions in draft status can be published.');
1695
        }
1696
1697
        $currentTime = $this->getUnixTimestamp();
1698
        if ($publicationDate === null && $versionInfo->versionNo === 1) {
1699
            $publicationDate = $currentTime;
1700
        }
1701
1702
        $metadataUpdateStruct = new SPIMetadataUpdateStruct();
1703
        $metadataUpdateStruct->publicationDate = $publicationDate;
1704
        $metadataUpdateStruct->modificationDate = $currentTime;
1705
1706
        $contentId = $versionInfo->getContentInfo()->id;
1707
        $spiContent = $this->persistenceHandler->contentHandler()->publish(
1708
            $contentId,
1709
            $versionInfo->versionNo,
1710
            $metadataUpdateStruct
1711
        );
1712
1713
        $content = $this->domainMapper->buildContentDomainObject(
1714
            $spiContent,
1715
            $this->repository->getContentTypeService()->loadContentType(
1716
                $spiContent->versionInfo->contentInfo->contentTypeId
1717
            )
1718
        );
1719
1720
        $this->publishUrlAliasesForContent($content);
1721
1722
        // Delete version archive overflow if any, limit is 0-50 (however 0 will mean 1 if content is unpublished)
1723
        $archiveList = $this->persistenceHandler->contentHandler()->listVersions(
1724
            $contentId,
1725
            APIVersionInfo::STATUS_ARCHIVED,
1726
            100 // Limited to avoid publishing taking to long, besides SE limitations this is why limit is max 50
1727
        );
1728
1729
        $maxVersionArchiveCount = max(0, min(50, $this->settings['default_version_archive_limit']));
1730
        while (!empty($archiveList) && count($archiveList) > $maxVersionArchiveCount) {
1731
            /** @var \eZ\Publish\SPI\Persistence\Content\VersionInfo $archiveVersion */
1732
            $archiveVersion = array_shift($archiveList);
1733
            $this->persistenceHandler->contentHandler()->deleteVersion(
1734
                $contentId,
1735
                $archiveVersion->versionNo
1736
            );
1737
        }
1738
1739
        return $content;
1740
    }
1741
1742
    /**
1743
     * @return int
1744
     */
1745
    protected function getUnixTimestamp()
1746
    {
1747
        return time();
1748
    }
1749
1750
    /**
1751
     * Removes the given version.
1752
     *
1753
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is in
1754
     *         published state or is a last version of Content in non draft state
1755
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to remove this version
1756
     *
1757
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1758
     */
1759
    public function deleteVersion(APIVersionInfo $versionInfo)
1760
    {
1761
        if ($versionInfo->isPublished()) {
1762
            throw new BadStateException(
1763
                '$versionInfo',
1764
                'Version is published and can not be removed'
1765
            );
1766
        }
1767
1768
        if (!$this->repository->canUser('content', 'versionremove', $versionInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1769
            throw new UnauthorizedException(
1770
                'content',
1771
                'versionremove',
1772
                ['contentId' => $versionInfo->contentInfo->id, 'versionNo' => $versionInfo->versionNo]
1773
            );
1774
        }
1775
1776
        $versionList = $this->persistenceHandler->contentHandler()->listVersions(
1777
            $versionInfo->contentInfo->id,
1778
            null,
1779
            2
1780
        );
1781
1782
        if (count($versionList) === 1 && !$versionInfo->isDraft()) {
1783
            throw new BadStateException(
1784
                '$versionInfo',
1785
                'Version is the last version of the Content and can not be removed'
1786
            );
1787
        }
1788
1789
        $this->repository->beginTransaction();
1790
        try {
1791
            $this->persistenceHandler->contentHandler()->deleteVersion(
1792
                $versionInfo->getContentInfo()->id,
1793
                $versionInfo->versionNo
1794
            );
1795
            $this->repository->commit();
1796
        } catch (Exception $e) {
1797
            $this->repository->rollback();
1798
            throw $e;
1799
        }
1800
    }
1801
1802
    /**
1803
     * Loads all versions for the given content.
1804
     *
1805
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to list versions
1806
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the given status is invalid
1807
     *
1808
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1809
     * @param int|null $status
1810
     *
1811
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Sorted by creation date
1812
     */
1813
    public function loadVersions(ContentInfo $contentInfo, ?int $status = null)
1814
    {
1815
        if (!$this->repository->canUser('content', 'versionread', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1816
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentInfo->id]);
1817
        }
1818
1819
        if ($status !== null && !in_array((int)$status, [VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED], true)) {
1820
            throw new InvalidArgumentException(
1821
                'status',
1822
                sprintf(
1823
                    'it can be one of %d (draft), %d (published), %d (archived), %d given',
1824
                    VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED, $status
1825
                ));
1826
        }
1827
1828
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->listVersions($contentInfo->id, $status);
1829
1830
        $versions = [];
1831
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1832
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1833
            if (!$this->repository->canUser('content', 'versionread', $versionInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1834
                throw new UnauthorizedException('content', 'versionread', ['versionId' => $versionInfo->id]);
1835
            }
1836
1837
            $versions[] = $versionInfo;
1838
        }
1839
1840
        return $versions;
1841
    }
1842
1843
    /**
1844
     * Copies the content to a new location. If no version is given,
1845
     * all versions are copied, otherwise only the given version.
1846
     *
1847
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to copy the content to the given location
1848
     *
1849
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1850
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $destinationLocationCreateStruct the target location where the content is copied to
1851
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1852
     *
1853
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1854
     */
1855
    public function copyContent(ContentInfo $contentInfo, LocationCreateStruct $destinationLocationCreateStruct, APIVersionInfo $versionInfo = null)
1856
    {
1857
        $destinationLocation = $this->repository->getLocationService()->loadLocation(
1858
            $destinationLocationCreateStruct->parentLocationId
1859
        );
1860
        if (!$this->repository->canUser('content', 'create', $contentInfo, [$destinationLocation])) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1861
            throw new UnauthorizedException(
1862
                'content',
1863
                'create',
1864
                [
1865
                    'parentLocationId' => $destinationLocationCreateStruct->parentLocationId,
1866
                    'sectionId' => $contentInfo->sectionId,
1867
                ]
1868
            );
1869
        }
1870
        if (!$this->repository->canUser('content', 'manage_locations', $contentInfo, [$destinationLocation])) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1871
            throw new UnauthorizedException('content', 'manage_locations', ['contentId' => $contentInfo->id]);
1872
        }
1873
1874
        $defaultObjectStates = $this->getDefaultObjectStates();
1875
1876
        $this->repository->beginTransaction();
1877
        try {
1878
            $spiContent = $this->persistenceHandler->contentHandler()->copy(
1879
                $contentInfo->id,
1880
                $versionInfo ? $versionInfo->versionNo : null,
1881
                $this->repository->getPermissionResolver()->getCurrentUserReference()->getUserId()
1882
            );
1883
1884
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
1885
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
1886
                $objectStateHandler->setContentState(
1887
                    $spiContent->versionInfo->contentInfo->id,
1888
                    $objectStateGroupId,
1889
                    $objectState->id
1890
                );
1891
            }
1892
1893
            $content = $this->internalPublishVersion(
1894
                $this->domainMapper->buildVersionInfoDomainObject($spiContent->versionInfo),
1895
                $spiContent->versionInfo->creationDate
1896
            );
1897
1898
            $this->repository->getLocationService()->createLocation(
1899
                $content->getVersionInfo()->getContentInfo(),
1900
                $destinationLocationCreateStruct
1901
            );
1902
            $this->repository->commit();
1903
        } catch (Exception $e) {
1904
            $this->repository->rollback();
1905
            throw $e;
1906
        }
1907
1908
        return $this->internalLoadContent($content->id);
1909
    }
1910
1911
    /**
1912
     * Loads all outgoing relations for the given version.
1913
     *
1914
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1915
     *
1916
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1917
     *
1918
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1919
     */
1920
    public function loadRelations(APIVersionInfo $versionInfo)
1921
    {
1922
        if ($versionInfo->isPublished()) {
1923
            $function = 'read';
1924
        } else {
1925
            $function = 'versionread';
1926
        }
1927
1928
        if (!$this->repository->canUser('content', $function, $versionInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1929
            throw new UnauthorizedException('content', $function);
1930
        }
1931
1932
        $contentInfo = $versionInfo->getContentInfo();
1933
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
1934
            $contentInfo->id,
1935
            $versionInfo->versionNo
1936
        );
1937
1938
        /** @var $relations \eZ\Publish\API\Repository\Values\Content\Relation[] */
1939
        $relations = [];
1940
        foreach ($spiRelations as $spiRelation) {
1941
            $destinationContentInfo = $this->internalLoadContentInfo($spiRelation->destinationContentId);
1942
            if (!$this->repository->canUser('content', 'read', $destinationContentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1943
                continue;
1944
            }
1945
1946
            $relations[] = $this->domainMapper->buildRelationDomainObject(
1947
                $spiRelation,
1948
                $contentInfo,
1949
                $destinationContentInfo
1950
            );
1951
        }
1952
1953
        return $relations;
1954
    }
1955
1956
    /**
1957
     * Loads all incoming relations for a content object.
1958
     *
1959
     * The relations come only from published versions of the source content objects
1960
     *
1961
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1962
     *
1963
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1964
     *
1965
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1966
     */
1967
    public function loadReverseRelations(ContentInfo $contentInfo)
1968
    {
1969
        if (!$this->repository->canUser('content', 'reverserelatedlist', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1970
            throw new UnauthorizedException('content', 'reverserelatedlist', ['contentId' => $contentInfo->id]);
1971
        }
1972
1973
        $spiRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations(
1974
            $contentInfo->id
1975
        );
1976
1977
        $returnArray = [];
1978
        foreach ($spiRelations as $spiRelation) {
1979
            $sourceContentInfo = $this->internalLoadContentInfo($spiRelation->sourceContentId);
1980
            if (!$this->repository->canUser('content', 'read', $sourceContentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1981
                continue;
1982
            }
1983
1984
            $returnArray[] = $this->domainMapper->buildRelationDomainObject(
1985
                $spiRelation,
1986
                $sourceContentInfo,
1987
                $contentInfo
1988
            );
1989
        }
1990
1991
        return $returnArray;
1992
    }
1993
1994
    /**
1995
     * Adds a relation of type common.
1996
     *
1997
     * The source of the relation is the content and version
1998
     * referenced by $versionInfo.
1999
     *
2000
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to edit this version
2001
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2002
     *
2003
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2004
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent the destination of the relation
2005
     *
2006
     * @return \eZ\Publish\API\Repository\Values\Content\Relation the newly created relation
2007
     */
2008
    public function addRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
2009
    {
2010
        $sourceVersion = $this->loadVersionInfoById(
2011
            $sourceVersion->contentInfo->id,
2012
            $sourceVersion->versionNo
2013
        );
2014
2015
        if (!$sourceVersion->isDraft()) {
2016
            throw new BadStateException(
2017
                '$sourceVersion',
2018
                'Relations of type common can only be added to versions of status draft'
2019
            );
2020
        }
2021
2022
        if (!$this->repository->canUser('content', 'edit', $sourceVersion)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2023
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2024
        }
2025
2026
        $sourceContentInfo = $sourceVersion->getContentInfo();
2027
2028
        $this->repository->beginTransaction();
2029
        try {
2030
            $spiRelation = $this->persistenceHandler->contentHandler()->addRelation(
2031
                new SPIRelationCreateStruct(
2032
                    [
2033
                        'sourceContentId' => $sourceContentInfo->id,
2034
                        'sourceContentVersionNo' => $sourceVersion->versionNo,
2035
                        'sourceFieldDefinitionId' => null,
2036
                        'destinationContentId' => $destinationContent->id,
2037
                        'type' => APIRelation::COMMON,
2038
                    ]
2039
                )
2040
            );
2041
            $this->repository->commit();
2042
        } catch (Exception $e) {
2043
            $this->repository->rollback();
2044
            throw $e;
2045
        }
2046
2047
        return $this->domainMapper->buildRelationDomainObject($spiRelation, $sourceContentInfo, $destinationContent);
2048
    }
2049
2050
    /**
2051
     * Removes a relation of type COMMON from a draft.
2052
     *
2053
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed edit this version
2054
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2055
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is no relation of type COMMON for the given destination
2056
     *
2057
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2058
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent
2059
     */
2060
    public function deleteRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
2061
    {
2062
        $sourceVersion = $this->loadVersionInfoById(
2063
            $sourceVersion->contentInfo->id,
2064
            $sourceVersion->versionNo
2065
        );
2066
2067
        if (!$sourceVersion->isDraft()) {
2068
            throw new BadStateException(
2069
                '$sourceVersion',
2070
                'Relations of type common can only be removed from versions of status draft'
2071
            );
2072
        }
2073
2074
        if (!$this->repository->canUser('content', 'edit', $sourceVersion)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2075
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2076
        }
2077
2078
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
2079
            $sourceVersion->getContentInfo()->id,
2080
            $sourceVersion->versionNo,
2081
            APIRelation::COMMON
2082
        );
2083
2084
        if (empty($spiRelations)) {
2085
            throw new InvalidArgumentException(
2086
                '$sourceVersion',
2087
                'There are no relations of type COMMON for the given destination'
2088
            );
2089
        }
2090
2091
        // there should be only one relation of type COMMON for each destination,
2092
        // but in case there were ever more then one, we will remove them all
2093
        // @todo: alternatively, throw BadStateException?
2094
        $this->repository->beginTransaction();
2095
        try {
2096
            foreach ($spiRelations as $spiRelation) {
2097
                if ($spiRelation->destinationContentId == $destinationContent->id) {
2098
                    $this->persistenceHandler->contentHandler()->removeRelation(
2099
                        $spiRelation->id,
2100
                        APIRelation::COMMON
2101
                    );
2102
                }
2103
            }
2104
            $this->repository->commit();
2105
        } catch (Exception $e) {
2106
            $this->repository->rollback();
2107
            throw $e;
2108
        }
2109
    }
2110
2111
    /**
2112
     * {@inheritdoc}
2113
     */
2114
    public function removeTranslation(ContentInfo $contentInfo, $languageCode)
2115
    {
2116
        @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...
2117
            __METHOD__ . ' is deprecated, use deleteTranslation instead',
2118
            E_USER_DEPRECATED
2119
        );
2120
        $this->deleteTranslation($contentInfo, $languageCode);
2121
    }
2122
2123
    /**
2124
     * Delete Content item Translation from all Versions (including archived ones) of a Content Object.
2125
     *
2126
     * NOTE: this operation is risky and permanent, so user interface should provide a warning before performing it.
2127
     *
2128
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2129
     *         is the Main Translation of a Content Item.
2130
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2131
     *         to delete the content (in one of the locations of the given Content Item).
2132
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2133
     *         is invalid for the given content.
2134
     *
2135
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2136
     * @param string $languageCode
2137
     *
2138
     * @since 6.13
2139
     */
2140
    public function deleteTranslation(ContentInfo $contentInfo, $languageCode)
2141
    {
2142
        if ($contentInfo->mainLanguageCode === $languageCode) {
2143
            throw new BadStateException(
2144
                '$languageCode',
2145
                'Specified translation is the main translation of the Content Object'
2146
            );
2147
        }
2148
2149
        $translationWasFound = false;
2150
        $this->repository->beginTransaction();
2151
        try {
2152
            foreach ($this->loadVersions($contentInfo) as $versionInfo) {
2153
                if (!$this->repository->canUser('content', 'remove', $versionInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2154
                    throw new UnauthorizedException(
2155
                        'content',
2156
                        'remove',
2157
                        ['contentId' => $contentInfo->id, 'versionNo' => $versionInfo->versionNo]
2158
                    );
2159
                }
2160
2161
                if (!in_array($languageCode, $versionInfo->languageCodes)) {
2162
                    continue;
2163
                }
2164
2165
                $translationWasFound = true;
2166
2167
                // If the translation is the version's only one, delete the version
2168
                if (count($versionInfo->languageCodes) < 2) {
2169
                    $this->persistenceHandler->contentHandler()->deleteVersion(
2170
                        $versionInfo->getContentInfo()->id,
2171
                        $versionInfo->versionNo
2172
                    );
2173
                }
2174
            }
2175
2176
            if (!$translationWasFound) {
2177
                throw new InvalidArgumentException(
2178
                    '$languageCode',
2179
                    sprintf(
2180
                        '%s does not exist in the Content item(id=%d)',
2181
                        $languageCode,
2182
                        $contentInfo->id
2183
                    )
2184
                );
2185
            }
2186
2187
            $this->persistenceHandler->contentHandler()->deleteTranslationFromContent(
2188
                $contentInfo->id,
2189
                $languageCode
2190
            );
2191
            $locationIds = array_map(
2192
                function (Location $location) {
2193
                    return $location->id;
2194
                },
2195
                $this->repository->getLocationService()->loadLocations($contentInfo)
2196
            );
2197
            $this->persistenceHandler->urlAliasHandler()->translationRemoved(
2198
                $locationIds,
2199
                $languageCode
2200
            );
2201
            $this->repository->commit();
2202
        } catch (InvalidArgumentException $e) {
2203
            $this->repository->rollback();
2204
            throw $e;
2205
        } catch (BadStateException $e) {
2206
            $this->repository->rollback();
2207
            throw $e;
2208
        } catch (UnauthorizedException $e) {
2209
            $this->repository->rollback();
2210
            throw $e;
2211
        } catch (Exception $e) {
2212
            $this->repository->rollback();
2213
            // cover generic unexpected exception to fulfill API promise on @throws
2214
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2215
        }
2216
    }
2217
2218
    /**
2219
     * Delete specified Translation from a Content Draft.
2220
     *
2221
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2222
     *         is the only one the Content Draft has or it is the main Translation of a Content Object.
2223
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2224
     *         to edit the Content (in one of the locations of the given Content Object).
2225
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2226
     *         is invalid for the given Draft.
2227
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if specified Version was not found
2228
     *
2229
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo Content Version Draft
2230
     * @param string $languageCode Language code of the Translation to be removed
2231
     *
2232
     * @return \eZ\Publish\API\Repository\Values\Content\Content Content Draft w/o the specified Translation
2233
     *
2234
     * @since 6.12
2235
     */
2236
    public function deleteTranslationFromDraft(APIVersionInfo $versionInfo, $languageCode)
2237
    {
2238
        if (!$versionInfo->isDraft()) {
2239
            throw new BadStateException(
2240
                '$versionInfo',
2241
                'Version is not a draft, so Translations cannot be modified. Create a Draft before proceeding'
2242
            );
2243
        }
2244
2245
        if ($versionInfo->contentInfo->mainLanguageCode === $languageCode) {
2246
            throw new BadStateException(
2247
                '$languageCode',
2248
                'Specified Translation is the main Translation of the Content Object. Change it before proceeding.'
2249
            );
2250
        }
2251
2252
        if (!$this->repository->canUser('content', 'edit', $versionInfo->contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2253
            throw new UnauthorizedException(
2254
                'content', 'edit', ['contentId' => $versionInfo->contentInfo->id]
2255
            );
2256
        }
2257
2258
        if (!in_array($languageCode, $versionInfo->languageCodes)) {
2259
            throw new InvalidArgumentException(
2260
                '$languageCode',
2261
                sprintf(
2262
                    'The Version (ContentId=%d, VersionNo=%d) is not translated into %s',
2263
                    $versionInfo->contentInfo->id,
2264
                    $versionInfo->versionNo,
2265
                    $languageCode
2266
                )
2267
            );
2268
        }
2269
2270
        if (count($versionInfo->languageCodes) === 1) {
2271
            throw new BadStateException(
2272
                '$languageCode',
2273
                'Specified Translation is the only one Content Object Version has'
2274
            );
2275
        }
2276
2277
        $this->repository->beginTransaction();
2278
        try {
2279
            $spiContent = $this->persistenceHandler->contentHandler()->deleteTranslationFromDraft(
2280
                $versionInfo->contentInfo->id,
2281
                $versionInfo->versionNo,
2282
                $languageCode
2283
            );
2284
            $this->repository->commit();
2285
2286
            return $this->domainMapper->buildContentDomainObject(
2287
                $spiContent,
2288
                $this->repository->getContentTypeService()->loadContentType(
2289
                    $spiContent->versionInfo->contentInfo->contentTypeId
2290
                )
2291
            );
2292
        } catch (APINotFoundException $e) {
2293
            // avoid wrapping expected NotFoundException in BadStateException handled below
2294
            $this->repository->rollback();
2295
            throw $e;
2296
        } catch (Exception $e) {
2297
            $this->repository->rollback();
2298
            // cover generic unexpected exception to fulfill API promise on @throws
2299
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2300
        }
2301
    }
2302
2303
    /**
2304
     * Hides Content by making all the Locations appear hidden.
2305
     * It does not persist hidden state on Location object itself.
2306
     *
2307
     * Content hidden by this API can be revealed by revealContent API.
2308
     *
2309
     * @see revealContent
2310
     *
2311
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2312
     */
2313
    public function hideContent(ContentInfo $contentInfo): void
2314
    {
2315
        if (!$this->repository->canUser('content', 'hide', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2316
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2317
        }
2318
2319
        $this->repository->beginTransaction();
2320
        try {
2321
            $this->persistenceHandler->contentHandler()->updateMetadata(
2322
                $contentInfo->id,
2323
                new SPIMetadataUpdateStruct([
2324
                    'isHidden' => true,
2325
                ])
2326
            );
2327
            $locationHandler = $this->persistenceHandler->locationHandler();
2328
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2329
            foreach ($childLocations as $childLocation) {
2330
                $locationHandler->setInvisible($childLocation->id);
2331
            }
2332
            $this->repository->commit();
2333
        } catch (Exception $e) {
2334
            $this->repository->rollback();
2335
            throw $e;
2336
        }
2337
    }
2338
2339
    /**
2340
     * Reveals Content hidden by hideContent API.
2341
     * Locations which were hidden before hiding Content will remain hidden.
2342
     *
2343
     * @see hideContent
2344
     *
2345
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2346
     */
2347
    public function revealContent(ContentInfo $contentInfo): void
2348
    {
2349
        if (!$this->repository->canUser('content', 'hide', $contentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2350
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2351
        }
2352
2353
        $this->repository->beginTransaction();
2354
        try {
2355
            $this->persistenceHandler->contentHandler()->updateMetadata(
2356
                $contentInfo->id,
2357
                new SPIMetadataUpdateStruct([
2358
                    'isHidden' => false,
2359
                ])
2360
            );
2361
            $locationHandler = $this->persistenceHandler->locationHandler();
2362
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2363
            foreach ($childLocations as $childLocation) {
2364
                $locationHandler->setVisible($childLocation->id);
2365
            }
2366
            $this->repository->commit();
2367
        } catch (Exception $e) {
2368
            $this->repository->rollback();
2369
            throw $e;
2370
        }
2371
    }
2372
2373
    /**
2374
     * Instantiates a new content create struct object.
2375
     *
2376
     * alwaysAvailable is set to the ContentType's defaultAlwaysAvailable
2377
     *
2378
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
2379
     * @param string $mainLanguageCode
2380
     *
2381
     * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
2382
     */
2383
    public function newContentCreateStruct(ContentType $contentType, $mainLanguageCode)
2384
    {
2385
        return new ContentCreateStruct(
2386
            [
2387
                'contentType' => $contentType,
2388
                'mainLanguageCode' => $mainLanguageCode,
2389
                'alwaysAvailable' => $contentType->defaultAlwaysAvailable,
2390
            ]
2391
        );
2392
    }
2393
2394
    /**
2395
     * Instantiates a new content meta data update struct.
2396
     *
2397
     * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
2398
     */
2399
    public function newContentMetadataUpdateStruct()
2400
    {
2401
        return new ContentMetadataUpdateStruct();
2402
    }
2403
2404
    /**
2405
     * Instantiates a new content update struct.
2406
     *
2407
     * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
2408
     */
2409
    public function newContentUpdateStruct()
2410
    {
2411
        return new ContentUpdateStruct();
2412
    }
2413
2414
    /**
2415
     * @param \eZ\Publish\API\Repository\Values\User\User|null $user
2416
     *
2417
     * @return \eZ\Publish\API\Repository\Values\User\UserReference
2418
     */
2419
    private function resolveUser(?User $user): UserReference
2420
    {
2421
        if ($user === null) {
2422
            $user = $this->repository->getPermissionResolver()->getCurrentUserReference();
2423
        }
2424
2425
        return $user;
2426
    }
2427
}
2428