Completed
Push — EZP-30969-fetch-reverse-relati... ( d71d24 )
by
unknown
121:16 queued 99:27
created

ContentService::copyContent()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 14
nop 3
dl 0
loc 55
rs 8.3595
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * File containing the eZ\Publish\Core\Repository\ContentService class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Repository;
10
11
use eZ\Publish\API\Repository\ContentService as ContentServiceInterface;
12
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
13
use eZ\Publish\API\Repository\Values\Content\RelationList;
14
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\RelationListItem;
15
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\UnauthorizedRelationListItem;
16
use eZ\Publish\Core\Repository\Values\Content\Location;
17
use eZ\Publish\API\Repository\Values\Content\Language;
18
use eZ\Publish\SPI\Persistence\Handler;
19
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
20
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
21
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
22
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
23
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
24
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
25
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
26
use eZ\Publish\API\Repository\Values\User\User;
27
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
28
use eZ\Publish\API\Repository\Values\Content\Field;
29
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
30
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
31
use eZ\Publish\Core\Base\Exceptions\BadStateException;
32
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
33
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
34
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
35
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
36
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
37
use eZ\Publish\Core\FieldType\ValidationError;
38
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
39
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
40
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
41
use eZ\Publish\SPI\Limitation\Target;
42
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
43
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
44
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
45
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
46
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
47
use Exception;
48
49
/**
50
 * This class provides service methods for managing content.
51
 *
52
 * @example Examples/content.php
53
 */
54
class ContentService implements ContentServiceInterface
55
{
56
    /** @var \eZ\Publish\Core\Repository\Repository */
57
    protected $repository;
58
59
    /** @var \eZ\Publish\SPI\Persistence\Handler */
60
    protected $persistenceHandler;
61
62
    /** @var array */
63
    protected $settings;
64
65
    /** @var \eZ\Publish\Core\Repository\Helper\DomainMapper */
66
    protected $domainMapper;
67
68
    /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
69
    protected $relationProcessor;
70
71
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
72
    protected $nameSchemaService;
73
74
    /** @var \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry */
75
    protected $fieldTypeRegistry;
76
77
    /**
78
     * Setups service with reference to repository object that created it & corresponding handler.
79
     *
80
     * @param \eZ\Publish\API\Repository\Repository $repository
81
     * @param \eZ\Publish\SPI\Persistence\Handler $handler
82
     * @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
83
     * @param \eZ\Publish\Core\Repository\Helper\RelationProcessor $relationProcessor
84
     * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
85
     * @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...
86
     * @param array $settings
87
     */
88
    public function __construct(
89
        RepositoryInterface $repository,
90
        Handler $handler,
91
        Helper\DomainMapper $domainMapper,
92
        Helper\RelationProcessor $relationProcessor,
93
        Helper\NameSchemaService $nameSchemaService,
94
        Helper\FieldTypeRegistry $fieldTypeRegistry,
95
        array $settings = []
96
    ) {
97
        $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...
98
        $this->persistenceHandler = $handler;
99
        $this->domainMapper = $domainMapper;
100
        $this->relationProcessor = $relationProcessor;
101
        $this->nameSchemaService = $nameSchemaService;
102
        $this->fieldTypeRegistry = $fieldTypeRegistry;
103
        // Union makes sure default settings are ignored if provided in argument
104
        $this->settings = $settings + [
105
            // Version archive limit (0-50), only enforced on publish, not on un-publish.
106
            'default_version_archive_limit' => 5,
107
        ];
108
    }
109
110
    /**
111
     * Loads a content info object.
112
     *
113
     * To load fields use loadContent
114
     *
115
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
116
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
117
     *
118
     * @param int $contentId
119
     *
120
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
121
     */
122
    public function loadContentInfo($contentId)
123
    {
124
        $contentInfo = $this->internalLoadContentInfo($contentId);
125
        if (!$this->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...
126
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
127
        }
128
129
        return $contentInfo;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function loadContentInfoList(array $contentIds): iterable
136
    {
137
        $contentInfoList = [];
138
        $spiInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
139
        foreach ($spiInfoList as $id => $spiInfo) {
140
            $contentInfo = $this->domainMapper->buildContentInfoDomainObject($spiInfo);
141
            if ($this->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...
142
                $contentInfoList[$id] = $contentInfo;
143
            }
144
        }
145
146
        return $contentInfoList;
147
    }
148
149
    /**
150
     * Loads a content info object.
151
     *
152
     * To load fields use loadContent
153
     *
154
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given id does not exist
155
     *
156
     * @param mixed $id
157
     * @param bool $isRemoteId
158
     *
159
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
160
     */
161
    public function internalLoadContentInfo($id, $isRemoteId = false)
162
    {
163
        try {
164
            $method = $isRemoteId ? 'loadContentInfoByRemoteId' : 'loadContentInfo';
165
166
            return $this->domainMapper->buildContentInfoDomainObject(
167
                $this->persistenceHandler->contentHandler()->$method($id)
168
            );
169
        } catch (APINotFoundException $e) {
170
            throw new NotFoundException(
171
                'Content',
172
                $id,
173
                $e
174
            );
175
        }
176
    }
177
178
    /**
179
     * Loads a content info object for the given remoteId.
180
     *
181
     * To load fields use loadContent
182
     *
183
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the content
184
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content with the given remote id does not exist
185
     *
186
     * @param string $remoteId
187
     *
188
     * @return \eZ\Publish\API\Repository\Values\Content\ContentInfo
189
     */
190
    public function loadContentInfoByRemoteId($remoteId)
191
    {
192
        $contentInfo = $this->internalLoadContentInfo($remoteId, true);
193
194
        if (!$this->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...
195
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
196
        }
197
198
        return $contentInfo;
199
    }
200
201
    /**
202
     * Loads a version info of the given content object.
203
     *
204
     * If no version number is given, the method returns the current version
205
     *
206
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
207
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
208
     *
209
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
210
     * @param int $versionNo the version number. If not given the current version is returned.
211
     *
212
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
213
     */
214
    public function loadVersionInfo(ContentInfo $contentInfo, $versionNo = null)
215
    {
216
        return $this->loadVersionInfoById($contentInfo->id, $versionNo);
217
    }
218
219
    /**
220
     * Loads a version info of the given content object id.
221
     *
222
     * If no version number is given, the method returns the current version
223
     *
224
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
225
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
226
     *
227
     * @param mixed $contentId
228
     * @param int $versionNo the version number. If not given the current version is returned.
229
     *
230
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
231
     */
232
    public function loadVersionInfoById($contentId, $versionNo = null)
233
    {
234
        try {
235
            $spiVersionInfo = $this->persistenceHandler->contentHandler()->loadVersionInfo(
236
                $contentId,
237
                $versionNo
238
            );
239
        } catch (APINotFoundException $e) {
240
            throw new NotFoundException(
241
                'VersionInfo',
242
                [
243
                    'contentId' => $contentId,
244
                    'versionNo' => $versionNo,
245
                ],
246
                $e
247
            );
248
        }
249
250
        $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
251
252
        if ($versionInfo->isPublished()) {
253
            $function = 'read';
254
        } else {
255
            $function = 'versionread';
256
        }
257
258
        if (!$this->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...
259
            throw new UnauthorizedException('content', $function, ['contentId' => $contentId]);
260
        }
261
262
        return $versionInfo;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
269
    {
270
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
271
        if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
272
            $useAlwaysAvailable = false;
273
        }
274
275
        return $this->loadContent(
276
            $contentInfo->id,
277
            $languages,
278
            $versionNo,// On purpose pass as-is and not use $contentInfo, to make sure to return actual current version on null
279
            $useAlwaysAvailable
280
        );
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages = null, $useAlwaysAvailable = true)
287
    {
288
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
289
        if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
290
            $useAlwaysAvailable = false;
291
        }
292
293
        return $this->loadContent(
294
            $versionInfo->getContentInfo()->id,
295
            $languages,
296
            $versionInfo->versionNo,
297
            $useAlwaysAvailable
298
        );
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function loadContent($contentId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
305
    {
306
        $content = $this->internalLoadContent($contentId, $languages, $versionNo, false, $useAlwaysAvailable);
307
308
        if (!$this->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...
309
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
310
        }
311
        if (
312
            !$content->getVersionInfo()->isPublished()
313
            && !$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...
314
        ) {
315
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId, 'versionNo' => $versionNo]);
316
        }
317
318
        return $content;
319
    }
320
321
    /**
322
     * Loads content in a version of the given content object.
323
     *
324
     * If no version number is given, the method returns the current version
325
     *
326
     * @internal
327
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the content or version with the given id and languages does not exist
328
     *
329
     * @param mixed $id
330
     * @param array|null $languages A language priority, filters returned fields and is used as prioritized language code on
331
     *                         returned value object. If not given all languages are returned.
332
     * @param int|null $versionNo the version number. If not given the current version is returned
333
     * @param bool $isRemoteId
334
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
335
     *
336
     * @return \eZ\Publish\API\Repository\Values\Content\Content
337
     */
338
    public function internalLoadContent($id, array $languages = null, $versionNo = null, $isRemoteId = false, $useAlwaysAvailable = true)
339
    {
340
        try {
341
            // Get Content ID if lookup by remote ID
342
            if ($isRemoteId) {
343
                $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($id);
344
                $id = $spiContentInfo->id;
345
                // Set $isRemoteId to false as the next loads will be for content id now that we have it (for exception use now)
346
                $isRemoteId = false;
347
            }
348
349
            $loadLanguages = $languages;
350
            $alwaysAvailableLanguageCode = null;
351
            // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
352
            // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
353
            if (!empty($loadLanguages) && $useAlwaysAvailable) {
354
                if (!isset($spiContentInfo)) {
355
                    $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
356
                }
357
358
                if ($spiContentInfo->alwaysAvailable) {
359
                    $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
360
                    $loadLanguages = array_unique($loadLanguages);
361
                }
362
            }
363
364
            $spiContent = $this->persistenceHandler->contentHandler()->load(
365
                $id,
366
                $versionNo,
367
                $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 349 can also be of type array; however, eZ\Publish\SPI\Persistence\Content\Handler::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

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

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

    return array();
}

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

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

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