Completed
Push — EZP-30969-fetch-reverse-relati... ( d71d24...a0ef69 )
by
unknown
137:48 queued 117:08
created

ContentService::countReverseRelations()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
rs 9.9332
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\Content\Type;
19
use eZ\Publish\SPI\Persistence\Handler;
20
use eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct as APIContentUpdateStruct;
21
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
22
use eZ\Publish\API\Repository\Values\Content\ContentCreateStruct as APIContentCreateStruct;
23
use eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct;
24
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
25
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
26
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
27
use eZ\Publish\API\Repository\Values\User\User;
28
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
29
use eZ\Publish\API\Repository\Values\Content\Field;
30
use eZ\Publish\API\Repository\Values\Content\Relation as APIRelation;
31
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
32
use eZ\Publish\Core\Base\Exceptions\BadStateException;
33
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
34
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
35
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
36
use eZ\Publish\Core\Base\Exceptions\ContentFieldValidationException;
37
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
38
use eZ\Publish\Core\FieldType\ValidationError;
39
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
40
use eZ\Publish\Core\Repository\Values\Content\ContentCreateStruct;
41
use eZ\Publish\Core\Repository\Values\Content\ContentUpdateStruct;
42
use eZ\Publish\SPI\Limitation\Target;
43
use eZ\Publish\SPI\Persistence\Content\MetadataUpdateStruct as SPIMetadataUpdateStruct;
44
use eZ\Publish\SPI\Persistence\Content\CreateStruct as SPIContentCreateStruct;
45
use eZ\Publish\SPI\Persistence\Content\UpdateStruct as SPIContentUpdateStruct;
46
use eZ\Publish\SPI\Persistence\Content\Field as SPIField;
47
use eZ\Publish\SPI\Persistence\Content\Relation\CreateStruct as SPIRelationCreateStruct;
48
use Exception;
49
50
/**
51
 * This class provides service methods for managing content.
52
 *
53
 * @example Examples/content.php
54
 */
55
class ContentService implements ContentServiceInterface
56
{
57
    /** @var \eZ\Publish\Core\Repository\Repository */
58
    protected $repository;
59
60
    /** @var \eZ\Publish\SPI\Persistence\Handler */
61
    protected $persistenceHandler;
62
63
    /** @var array */
64
    protected $settings;
65
66
    /** @var \eZ\Publish\Core\Repository\Helper\DomainMapper */
67
    protected $domainMapper;
68
69
    /** @var \eZ\Publish\Core\Repository\Helper\RelationProcessor */
70
    protected $relationProcessor;
71
72
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
73
    protected $nameSchemaService;
74
75
    /** @var \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry */
76
    protected $fieldTypeRegistry;
77
78
    /**
79
     * Setups service with reference to repository object that created it & corresponding handler.
80
     *
81
     * @param \eZ\Publish\API\Repository\Repository $repository
82
     * @param \eZ\Publish\SPI\Persistence\Handler $handler
83
     * @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
84
     * @param \eZ\Publish\Core\Repository\Helper\RelationProcessor $relationProcessor
85
     * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
86
     * @param \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry $fieldTypeRegistry,
0 ignored issues
show
Documentation introduced by
There is no parameter named $fieldTypeRegistry,. Did you maybe mean $fieldTypeRegistry?

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

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

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

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

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

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

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

class Alien {}

class Dalek extends Alien {}

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

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

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

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

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

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

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

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

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

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

Loading history...
196
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
197
        }
198
199
        return $contentInfo;
200
    }
201
202
    /**
203
     * Loads a version info of the given content object.
204
     *
205
     * If no version number is given, the method returns the current version
206
     *
207
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
208
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
209
     *
210
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
211
     * @param int $versionNo the version number. If not given the current version is returned.
212
     *
213
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
214
     */
215
    public function loadVersionInfo(ContentInfo $contentInfo, $versionNo = null)
216
    {
217
        return $this->loadVersionInfoById($contentInfo->id, $versionNo);
218
    }
219
220
    /**
221
     * Loads a version info of the given content object id.
222
     *
223
     * If no version number is given, the method returns the current version
224
     *
225
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the version with the given number does not exist
226
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to load this version
227
     *
228
     * @param mixed $contentId
229
     * @param int $versionNo the version number. If not given the current version is returned.
230
     *
231
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo
232
     */
233
    public function loadVersionInfoById($contentId, $versionNo = null)
234
    {
235
        try {
236
            $spiVersionInfo = $this->persistenceHandler->contentHandler()->loadVersionInfo(
237
                $contentId,
238
                $versionNo
239
            );
240
        } catch (APINotFoundException $e) {
241
            throw new NotFoundException(
242
                'VersionInfo',
243
                [
244
                    'contentId' => $contentId,
245
                    'versionNo' => $versionNo,
246
                ],
247
                $e
248
            );
249
        }
250
251
        $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
252
253
        if ($versionInfo->isPublished()) {
254
            $function = 'read';
255
        } else {
256
            $function = 'versionread';
257
        }
258
259
        if (!$this->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...
260
            throw new UnauthorizedException('content', $function, ['contentId' => $contentId]);
261
        }
262
263
        return $versionInfo;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function loadContentByContentInfo(ContentInfo $contentInfo, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
270
    {
271
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
272
        if ($useAlwaysAvailable && !$contentInfo->alwaysAvailable) {
273
            $useAlwaysAvailable = false;
274
        }
275
276
        return $this->loadContent(
277
            $contentInfo->id,
278
            $languages,
279
            $versionNo,// On purpose pass as-is and not use $contentInfo, to make sure to return actual current version on null
280
            $useAlwaysAvailable
281
        );
282
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287
    public function loadContentByVersionInfo(APIVersionInfo $versionInfo, array $languages = null, $useAlwaysAvailable = true)
288
    {
289
        // Change $useAlwaysAvailable to false to avoid contentInfo lookup if we know alwaysAvailable is disabled
290
        if ($useAlwaysAvailable && !$versionInfo->getContentInfo()->alwaysAvailable) {
291
            $useAlwaysAvailable = false;
292
        }
293
294
        return $this->loadContent(
295
            $versionInfo->getContentInfo()->id,
296
            $languages,
297
            $versionInfo->versionNo,
298
            $useAlwaysAvailable
299
        );
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305
    public function loadContent($contentId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
306
    {
307
        $content = $this->internalLoadContent($contentId, $languages, $versionNo, false, $useAlwaysAvailable);
308
309
        if (!$this->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...
310
            throw new UnauthorizedException('content', 'read', ['contentId' => $contentId]);
311
        }
312
        if (
313
            !$content->getVersionInfo()->isPublished()
314
            && !$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...
315
        ) {
316
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId, 'versionNo' => $versionNo]);
317
        }
318
319
        return $content;
320
    }
321
322
    /**
323
     * Loads content in a version of the given content object.
324
     *
325
     * If no version number is given, the method returns the current version
326
     *
327
     * @internal
328
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the content or version with the given id and languages does not exist
329
     *
330
     * @param mixed $id
331
     * @param array|null $languages A language priority, filters returned fields and is used as prioritized language code on
332
     *                         returned value object. If not given all languages are returned.
333
     * @param int|null $versionNo the version number. If not given the current version is returned
334
     * @param bool $isRemoteId
335
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
336
     *
337
     * @return \eZ\Publish\API\Repository\Values\Content\Content
338
     */
339
    public function internalLoadContent($id, array $languages = null, $versionNo = null, $isRemoteId = false, $useAlwaysAvailable = true)
340
    {
341
        try {
342
            // Get Content ID if lookup by remote ID
343
            if ($isRemoteId) {
344
                $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($id);
345
                $id = $spiContentInfo->id;
346
                // Set $isRemoteId to false as the next loads will be for content id now that we have it (for exception use now)
347
                $isRemoteId = false;
348
            }
349
350
            $loadLanguages = $languages;
351
            $alwaysAvailableLanguageCode = null;
352
            // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
353
            // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
354
            if (!empty($loadLanguages) && $useAlwaysAvailable) {
355
                if (!isset($spiContentInfo)) {
356
                    $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
357
                }
358
359
                if ($spiContentInfo->alwaysAvailable) {
360
                    $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
361
                    $loadLanguages = array_unique($loadLanguages);
362
                }
363
            }
364
365
            $spiContent = $this->persistenceHandler->contentHandler()->load(
366
                $id,
367
                $versionNo,
368
                $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 350 can also be of type array; however, eZ\Publish\SPI\Persistence\Content\Handler::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
369
            );
370
        } catch (APINotFoundException $e) {
371
            throw new NotFoundException(
372
                'Content',
373
                [
374
                    $isRemoteId ? 'remoteId' : 'id' => $id,
375
                    'languages' => $languages,
376
                    'versionNo' => $versionNo,
377
                ],
378
                $e
379
            );
380
        }
381
382
        return $this->domainMapper->buildContentDomainObject(
383
            $spiContent,
384
            $this->repository->getContentTypeService()->loadContentType(
385
                $spiContent->versionInfo->contentInfo->contentTypeId
386
            ),
387
            $languages ?? [],
388
            $alwaysAvailableLanguageCode
389
        );
390
    }
391
392
    /**
393
     * Loads content in a version for the content object reference by the given remote id.
394
     *
395
     * If no version is given, the method returns the current version
396
     *
397
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content or version with the given remote id does not exist
398
     * @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
399
     *
400
     * @param string $remoteId
401
     * @param array $languages A language filter for fields. If not given all languages are returned
402
     * @param int $versionNo the version number. If not given the current version is returned
403
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
404
     *
405
     * @return \eZ\Publish\API\Repository\Values\Content\Content
406
     */
407
    public function loadContentByRemoteId($remoteId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
408
    {
409
        $content = $this->internalLoadContent($remoteId, $languages, $versionNo, true, $useAlwaysAvailable);
410
411
        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...
412
            throw new UnauthorizedException('content', 'read', ['remoteId' => $remoteId]);
413
        }
414
415
        if (
416
            !$content->getVersionInfo()->isPublished()
417
            && !$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...
418
        ) {
419
            throw new UnauthorizedException('content', 'versionread', ['remoteId' => $remoteId, 'versionNo' => $versionNo]);
420
        }
421
422
        return $content;
423
    }
424
425
    /**
426
     * Bulk-load Content items by the list of ContentInfo Value Objects.
427
     *
428
     * Note: it does not throw exceptions on load, just ignores erroneous Content item.
429
     * Moreover, since the method works on pre-loaded ContentInfo list, it is assumed that user is
430
     * allowed to access every Content on the list.
431
     *
432
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo[] $contentInfoList
433
     * @param string[] $languages A language priority, filters returned fields and is used as prioritized language code on
434
     *                            returned value object. If not given all languages are returned.
435
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true,
436
     *                                 unless all languages have been asked for.
437
     *
438
     * @return \eZ\Publish\API\Repository\Values\Content\Content[] list of Content items with Content Ids as keys
439
     */
440
    public function loadContentListByContentInfo(
441
        array $contentInfoList,
442
        array $languages = [],
443
        $useAlwaysAvailable = true
444
    ) {
445
        $loadAllLanguages = $languages === Language::ALL;
446
        $contentIds = [];
447
        $contentTypeIds = [];
448
        $translations = $languages;
449
        foreach ($contentInfoList as $contentInfo) {
450
            $contentIds[] = $contentInfo->id;
451
            $contentTypeIds[] = $contentInfo->contentTypeId;
452
            // Unless we are told to load all languages, we add main language to translations so they are loaded too
453
            // Might in some case load more languages then intended, but prioritised handling will pick right one
454
            if (!$loadAllLanguages && $useAlwaysAvailable && $contentInfo->alwaysAvailable) {
455
                $translations[] = $contentInfo->mainLanguageCode;
456
            }
457
        }
458
459
        $contentList = [];
460
        $translations = array_unique($translations);
461
        $spiContentList = $this->persistenceHandler->contentHandler()->loadContentList(
462
            $contentIds,
463
            $translations
464
        );
465
        $contentTypeList = $this->repository->getContentTypeService()->loadContentTypeList(
466
            array_unique($contentTypeIds),
467
            $languages
468
        );
469
        foreach ($spiContentList as $contentId => $spiContent) {
470
            $contentInfo = $spiContent->versionInfo->contentInfo;
471
            $contentList[$contentId] = $this->domainMapper->buildContentDomainObject(
472
                $spiContent,
473
                $contentTypeList[$contentInfo->contentTypeId],
474
                $languages,
475
                $contentInfo->alwaysAvailable ? $contentInfo->mainLanguageCode : null
476
            );
477
        }
478
479
        return $contentList;
480
    }
481
482
    /**
483
     * Creates a new content draft assigned to the authenticated user.
484
     *
485
     * If a different userId is given in $contentCreateStruct it is assigned to the given user
486
     * but this required special rights for the authenticated user
487
     * (this is useful for content staging where the transfer process does not
488
     * have to authenticate with the user which created the content object in the source server).
489
     * The user has to publish the draft if it should be visible.
490
     * In 4.x at least one location has to be provided in the location creation array.
491
     *
492
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to create the content in the given location
493
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the provided remoteId exists in the system, required properties on
494
     *                                                                        struct are missing or invalid, or if multiple locations are under the
495
     *                                                                        same parent.
496
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
497
     *                                                                               or if a required field is missing / set to an empty value.
498
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
499
     *                                                                          or value is set for non-translatable field in language
500
     *                                                                          other than main.
501
     *
502
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
503
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs For each location parent under which a location should be created for the content
504
     *
505
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
506
     */
507
    public function createContent(APIContentCreateStruct $contentCreateStruct, array $locationCreateStructs = [])
508
    {
509
        if ($contentCreateStruct->mainLanguageCode === null) {
510
            throw new InvalidArgumentException('$contentCreateStruct', "'mainLanguageCode' property must be set");
511
        }
512
513
        if ($contentCreateStruct->contentType === null) {
514
            throw new InvalidArgumentException('$contentCreateStruct', "'contentType' property must be set");
515
        }
516
517
        $contentCreateStruct = clone $contentCreateStruct;
518
519
        if ($contentCreateStruct->ownerId === null) {
520
            $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...
521
        }
522
523
        if ($contentCreateStruct->alwaysAvailable === null) {
524
            $contentCreateStruct->alwaysAvailable = $contentCreateStruct->contentType->defaultAlwaysAvailable ?: false;
525
        }
526
527
        $contentCreateStruct->contentType = $this->repository->getContentTypeService()->loadContentType(
528
            $contentCreateStruct->contentType->id
529
        );
530
531
        if (empty($contentCreateStruct->sectionId)) {
532
            if (isset($locationCreateStructs[0])) {
533
                $location = $this->repository->getLocationService()->loadLocation(
534
                    $locationCreateStructs[0]->parentLocationId
535
                );
536
                $contentCreateStruct->sectionId = $location->contentInfo->sectionId;
537
            } else {
538
                $contentCreateStruct->sectionId = 1;
539
            }
540
        }
541
542
        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...
543
            throw new UnauthorizedException(
544
                'content',
545
                'create',
546
                [
547
                    'parentLocationId' => isset($locationCreateStructs[0]) ?
548
                            $locationCreateStructs[0]->parentLocationId :
549
                            null,
550
                    'sectionId' => $contentCreateStruct->sectionId,
551
                ]
552
            );
553
        }
554
555
        if (!empty($contentCreateStruct->remoteId)) {
556
            try {
557
                $this->loadContentByRemoteId($contentCreateStruct->remoteId);
558
559
                throw new InvalidArgumentException(
560
                    '$contentCreateStruct',
561
                    "Another content with remoteId '{$contentCreateStruct->remoteId}' exists"
562
                );
563
            } catch (APINotFoundException $e) {
564
                // Do nothing
565
            }
566
        } else {
567
            $contentCreateStruct->remoteId = $this->domainMapper->getUniqueHash($contentCreateStruct);
568
        }
569
570
        $spiLocationCreateStructs = $this->buildSPILocationCreateStructs($locationCreateStructs);
571
572
        $languageCodes = $this->getLanguageCodesForCreate($contentCreateStruct);
573
        $fields = $this->mapFieldsForCreate($contentCreateStruct);
574
575
        $fieldValues = [];
576
        $spiFields = [];
577
        $allFieldErrors = [];
578
        $inputRelations = [];
579
        $locationIdToContentIdMapping = [];
580
581
        foreach ($contentCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
582
            /** @var $fieldType \eZ\Publish\Core\FieldType\FieldType */
583
            $fieldType = $this->fieldTypeRegistry->getFieldType(
584
                $fieldDefinition->fieldTypeIdentifier
585
            );
586
587
            foreach ($languageCodes as $languageCode) {
588
                $isEmptyValue = false;
589
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $contentCreateStruct->mainLanguageCode;
590
                $isLanguageMain = $languageCode === $contentCreateStruct->mainLanguageCode;
591
                if (isset($fields[$fieldDefinition->identifier][$valueLanguageCode])) {
592
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
593
                } else {
594
                    $fieldValue = $fieldDefinition->defaultValue;
595
                }
596
597
                $fieldValue = $fieldType->acceptValue($fieldValue);
598
599
                if ($fieldType->isEmptyValue($fieldValue)) {
600
                    $isEmptyValue = true;
601
                    if ($fieldDefinition->isRequired) {
602
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
603
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
604
                            null,
605
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
606
                            'empty'
607
                        );
608
                    }
609
                } else {
610
                    $fieldErrors = $fieldType->validate(
611
                        $fieldDefinition,
612
                        $fieldValue
613
                    );
614
                    if (!empty($fieldErrors)) {
615
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
616
                    }
617
                }
618
619
                if (!empty($allFieldErrors)) {
620
                    continue;
621
                }
622
623
                $this->relationProcessor->appendFieldRelations(
624
                    $inputRelations,
625
                    $locationIdToContentIdMapping,
626
                    $fieldType,
627
                    $fieldValue,
628
                    $fieldDefinition->id
629
                );
630
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
631
632
                // Only non-empty value for: translatable field or in main language
633
                if (
634
                    (!$isEmptyValue && $fieldDefinition->isTranslatable) ||
635
                    (!$isEmptyValue && $isLanguageMain)
636
                ) {
637
                    $spiFields[] = new SPIField(
638
                        [
639
                            'id' => null,
640
                            'fieldDefinitionId' => $fieldDefinition->id,
641
                            'type' => $fieldDefinition->fieldTypeIdentifier,
642
                            'value' => $fieldType->toPersistenceValue($fieldValue),
643
                            'languageCode' => $languageCode,
644
                            'versionNo' => null,
645
                        ]
646
                    );
647
                }
648
            }
649
        }
650
651
        if (!empty($allFieldErrors)) {
652
            throw new ContentFieldValidationException($allFieldErrors);
653
        }
654
655
        $spiContentCreateStruct = new SPIContentCreateStruct(
656
            [
657
                'name' => $this->nameSchemaService->resolve(
658
                    $contentCreateStruct->contentType->nameSchema,
659
                    $contentCreateStruct->contentType,
660
                    $fieldValues,
661
                    $languageCodes
662
                ),
663
                'typeId' => $contentCreateStruct->contentType->id,
664
                'sectionId' => $contentCreateStruct->sectionId,
665
                'ownerId' => $contentCreateStruct->ownerId,
666
                'locations' => $spiLocationCreateStructs,
667
                'fields' => $spiFields,
668
                'alwaysAvailable' => $contentCreateStruct->alwaysAvailable,
669
                'remoteId' => $contentCreateStruct->remoteId,
670
                'modified' => isset($contentCreateStruct->modificationDate) ? $contentCreateStruct->modificationDate->getTimestamp() : time(),
671
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
672
                    $contentCreateStruct->mainLanguageCode
673
                )->id,
674
            ]
675
        );
676
677
        $defaultObjectStates = $this->getDefaultObjectStates();
678
679
        $this->repository->beginTransaction();
680
        try {
681
            $spiContent = $this->persistenceHandler->contentHandler()->create($spiContentCreateStruct);
682
            $this->relationProcessor->processFieldRelations(
683
                $inputRelations,
684
                $spiContent->versionInfo->contentInfo->id,
685
                $spiContent->versionInfo->versionNo,
686
                $contentCreateStruct->contentType
687
            );
688
689
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
690
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
691
                $objectStateHandler->setContentState(
692
                    $spiContent->versionInfo->contentInfo->id,
693
                    $objectStateGroupId,
694
                    $objectState->id
695
                );
696
            }
697
698
            $this->repository->commit();
699
        } catch (Exception $e) {
700
            $this->repository->rollback();
701
            throw $e;
702
        }
703
704
        return $this->domainMapper->buildContentDomainObject(
705
            $spiContent,
706
            $contentCreateStruct->contentType
707
        );
708
    }
709
710
    /**
711
     * Returns an array of default content states with content state group id as key.
712
     *
713
     * @return \eZ\Publish\SPI\Persistence\Content\ObjectState[]
714
     */
715
    protected function getDefaultObjectStates()
716
    {
717
        $defaultObjectStatesMap = [];
718
        $objectStateHandler = $this->persistenceHandler->objectStateHandler();
719
720
        foreach ($objectStateHandler->loadAllGroups() as $objectStateGroup) {
721
            foreach ($objectStateHandler->loadObjectStates($objectStateGroup->id) as $objectState) {
722
                // Only register the first object state which is the default one.
723
                $defaultObjectStatesMap[$objectStateGroup->id] = $objectState;
724
                break;
725
            }
726
        }
727
728
        return $defaultObjectStatesMap;
729
    }
730
731
    /**
732
     * Returns all language codes used in given $fields.
733
     *
734
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value is set in main language
735
     *
736
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
737
     *
738
     * @return string[]
739
     */
740
    protected function getLanguageCodesForCreate(APIContentCreateStruct $contentCreateStruct)
741
    {
742
        $languageCodes = [];
743
744
        foreach ($contentCreateStruct->fields as $field) {
745
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
746
                continue;
747
            }
748
749
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
750
                $field->languageCode
751
            );
752
            $languageCodes[$field->languageCode] = true;
753
        }
754
755
        if (!isset($languageCodes[$contentCreateStruct->mainLanguageCode])) {
756
            $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
757
                $contentCreateStruct->mainLanguageCode
758
            );
759
            $languageCodes[$contentCreateStruct->mainLanguageCode] = true;
760
        }
761
762
        return array_keys($languageCodes);
763
    }
764
765
    /**
766
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
767
     *
768
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
769
     *                                                                          or value is set for non-translatable field in language
770
     *                                                                          other than main
771
     *
772
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $contentCreateStruct
773
     *
774
     * @return array
775
     */
776
    protected function mapFieldsForCreate(APIContentCreateStruct $contentCreateStruct)
777
    {
778
        $fields = [];
779
780
        foreach ($contentCreateStruct->fields as $field) {
781
            $fieldDefinition = $contentCreateStruct->contentType->getFieldDefinition($field->fieldDefIdentifier);
782
783
            if ($fieldDefinition === null) {
784
                throw new ContentValidationException(
785
                    "Field definition '%identifier%' does not exist in given ContentType",
786
                    ['%identifier%' => $field->fieldDefIdentifier]
787
                );
788
            }
789
790
            if ($field->languageCode === null) {
791
                $field = $this->cloneField(
792
                    $field,
793
                    ['languageCode' => $contentCreateStruct->mainLanguageCode]
794
                );
795
            }
796
797
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $contentCreateStruct->mainLanguageCode)) {
798
                throw new ContentValidationException(
799
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
800
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
801
                );
802
            }
803
804
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
805
        }
806
807
        return $fields;
808
    }
809
810
    /**
811
     * Clones $field with overriding specific properties from given $overrides array.
812
     *
813
     * @param Field $field
814
     * @param array $overrides
815
     *
816
     * @return Field
817
     */
818
    private function cloneField(Field $field, array $overrides = [])
819
    {
820
        $fieldData = array_merge(
821
            [
822
                'id' => $field->id,
823
                'value' => $field->value,
824
                'languageCode' => $field->languageCode,
825
                'fieldDefIdentifier' => $field->fieldDefIdentifier,
826
                'fieldTypeIdentifier' => $field->fieldTypeIdentifier,
827
            ],
828
            $overrides
829
        );
830
831
        return new Field($fieldData);
832
    }
833
834
    /**
835
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
836
     *
837
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationCreateStructs
838
     *
839
     * @return \eZ\Publish\SPI\Persistence\Content\Location\CreateStruct[]
840
     */
841
    protected function buildSPILocationCreateStructs(array $locationCreateStructs)
842
    {
843
        $spiLocationCreateStructs = [];
844
        $parentLocationIdSet = [];
845
        $mainLocation = true;
846
847
        foreach ($locationCreateStructs as $locationCreateStruct) {
848
            if (isset($parentLocationIdSet[$locationCreateStruct->parentLocationId])) {
849
                throw new InvalidArgumentException(
850
                    '$locationCreateStructs',
851
                    "Multiple LocationCreateStructs with the same parent Location '{$locationCreateStruct->parentLocationId}' are given"
852
                );
853
            }
854
855
            if (!array_key_exists($locationCreateStruct->sortField, Location::SORT_FIELD_MAP)) {
856
                $locationCreateStruct->sortField = Location::SORT_FIELD_NAME;
857
            }
858
859
            if (!array_key_exists($locationCreateStruct->sortOrder, Location::SORT_ORDER_MAP)) {
860
                $locationCreateStruct->sortOrder = Location::SORT_ORDER_ASC;
861
            }
862
863
            $parentLocationIdSet[$locationCreateStruct->parentLocationId] = true;
864
            $parentLocation = $this->repository->getLocationService()->loadLocation(
865
                $locationCreateStruct->parentLocationId
866
            );
867
868
            $spiLocationCreateStructs[] = $this->domainMapper->buildSPILocationCreateStruct(
869
                $locationCreateStruct,
870
                $parentLocation,
871
                $mainLocation,
872
                // For Content draft contentId and contentVersionNo are set in ContentHandler upon draft creation
873
                null,
874
                null
875
            );
876
877
            // First Location in the list will be created as main Location
878
            $mainLocation = false;
879
        }
880
881
        return $spiLocationCreateStructs;
882
    }
883
884
    /**
885
     * Updates the metadata.
886
     *
887
     * (see {@link ContentMetadataUpdateStruct}) of a content object - to update fields use updateContent
888
     *
889
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update the content meta data
890
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the remoteId in $contentMetadataUpdateStruct is set but already exists
891
     *
892
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
893
     * @param \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct $contentMetadataUpdateStruct
894
     *
895
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content with the updated attributes
896
     */
897
    public function updateContentMetadata(ContentInfo $contentInfo, ContentMetadataUpdateStruct $contentMetadataUpdateStruct)
898
    {
899
        $propertyCount = 0;
900
        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...
901
            if (isset($contentMetadataUpdateStruct->$propertyName)) {
902
                $propertyCount += 1;
903
            }
904
        }
905
        if ($propertyCount === 0) {
906
            throw new InvalidArgumentException(
907
                '$contentMetadataUpdateStruct',
908
                'At least one property must be set'
909
            );
910
        }
911
912
        $loadedContentInfo = $this->loadContentInfo($contentInfo->id);
913
914
        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...
915
            throw new UnauthorizedException('content', 'edit', ['contentId' => $loadedContentInfo->id]);
916
        }
917
918
        if (isset($contentMetadataUpdateStruct->remoteId)) {
919
            try {
920
                $existingContentInfo = $this->loadContentInfoByRemoteId($contentMetadataUpdateStruct->remoteId);
921
922
                if ($existingContentInfo->id !== $loadedContentInfo->id) {
923
                    throw new InvalidArgumentException(
924
                        '$contentMetadataUpdateStruct',
925
                        "Another content with remoteId '{$contentMetadataUpdateStruct->remoteId}' exists"
926
                    );
927
                }
928
            } catch (APINotFoundException $e) {
929
                // Do nothing
930
            }
931
        }
932
933
        $this->repository->beginTransaction();
934
        try {
935
            if ($propertyCount > 1 || !isset($contentMetadataUpdateStruct->mainLocationId)) {
936
                $this->persistenceHandler->contentHandler()->updateMetadata(
937
                    $loadedContentInfo->id,
938
                    new SPIMetadataUpdateStruct(
939
                        [
940
                            'ownerId' => $contentMetadataUpdateStruct->ownerId,
941
                            'publicationDate' => isset($contentMetadataUpdateStruct->publishedDate) ?
942
                                $contentMetadataUpdateStruct->publishedDate->getTimestamp() :
943
                                null,
944
                            'modificationDate' => isset($contentMetadataUpdateStruct->modificationDate) ?
945
                                $contentMetadataUpdateStruct->modificationDate->getTimestamp() :
946
                                null,
947
                            'mainLanguageId' => isset($contentMetadataUpdateStruct->mainLanguageCode) ?
948
                                $this->repository->getContentLanguageService()->loadLanguage(
949
                                    $contentMetadataUpdateStruct->mainLanguageCode
950
                                )->id :
951
                                null,
952
                            'alwaysAvailable' => $contentMetadataUpdateStruct->alwaysAvailable,
953
                            'remoteId' => $contentMetadataUpdateStruct->remoteId,
954
                            'name' => $contentMetadataUpdateStruct->name,
955
                        ]
956
                    )
957
                );
958
            }
959
960
            // Change main location
961
            if (isset($contentMetadataUpdateStruct->mainLocationId)
962
                && $loadedContentInfo->mainLocationId !== $contentMetadataUpdateStruct->mainLocationId) {
963
                $this->persistenceHandler->locationHandler()->changeMainLocation(
964
                    $loadedContentInfo->id,
965
                    $contentMetadataUpdateStruct->mainLocationId
966
                );
967
            }
968
969
            // Republish URL aliases to update always-available flag
970
            if (isset($contentMetadataUpdateStruct->alwaysAvailable)
971
                && $loadedContentInfo->alwaysAvailable !== $contentMetadataUpdateStruct->alwaysAvailable) {
972
                $content = $this->loadContent($loadedContentInfo->id);
973
                $this->publishUrlAliasesForContent($content, false);
974
            }
975
976
            $this->repository->commit();
977
        } catch (Exception $e) {
978
            $this->repository->rollback();
979
            throw $e;
980
        }
981
982
        return isset($content) ? $content : $this->loadContent($loadedContentInfo->id);
983
    }
984
985
    /**
986
     * Publishes URL aliases for all locations of a given content.
987
     *
988
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
989
     * @param bool $updatePathIdentificationString this parameter is legacy storage specific for updating
990
     *                      ezcontentobject_tree.path_identification_string, it is ignored by other storage engines
991
     */
992
    protected function publishUrlAliasesForContent(APIContent $content, $updatePathIdentificationString = true)
993
    {
994
        $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
995
        $locations = $this->repository->getLocationService()->loadLocations(
996
            $content->getVersionInfo()->getContentInfo()
997
        );
998
        $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
999
        foreach ($locations as $location) {
1000
            foreach ($urlAliasNames as $languageCode => $name) {
1001
                $urlAliasHandler->publishUrlAliasForLocation(
1002
                    $location->id,
1003
                    $location->parentLocationId,
1004
                    $name,
1005
                    $languageCode,
1006
                    $content->contentInfo->alwaysAvailable,
1007
                    $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...
1008
                );
1009
            }
1010
            // archive URL aliases of Translations that got deleted
1011
            $urlAliasHandler->archiveUrlAliasesForDeletedTranslations(
1012
                $location->id,
1013
                $location->parentLocationId,
1014
                $content->versionInfo->languageCodes
1015
            );
1016
        }
1017
    }
1018
1019
    /**
1020
     * Deletes a content object including all its versions and locations including their subtrees.
1021
     *
1022
     * @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)
1023
     *
1024
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1025
     *
1026
     * @return mixed[] Affected Location Id's
1027
     */
1028
    public function deleteContent(ContentInfo $contentInfo)
1029
    {
1030
        $contentInfo = $this->internalLoadContentInfo($contentInfo->id);
1031
1032
        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...
1033
            throw new UnauthorizedException('content', 'remove', ['contentId' => $contentInfo->id]);
1034
        }
1035
1036
        $affectedLocations = [];
1037
        $this->repository->beginTransaction();
1038
        try {
1039
            // Load Locations first as deleting Content also deletes belonging Locations
1040
            $spiLocations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentInfo->id);
1041
            $this->persistenceHandler->contentHandler()->deleteContent($contentInfo->id);
1042
            $urlAliasHandler = $this->persistenceHandler->urlAliasHandler();
1043
            foreach ($spiLocations as $spiLocation) {
1044
                $urlAliasHandler->locationDeleted($spiLocation->id);
1045
                $affectedLocations[] = $spiLocation->id;
1046
            }
1047
            $this->repository->commit();
1048
        } catch (Exception $e) {
1049
            $this->repository->rollback();
1050
            throw $e;
1051
        }
1052
1053
        return $affectedLocations;
1054
    }
1055
1056
    /**
1057
     * Creates a draft from a published or archived version.
1058
     *
1059
     * If no version is given, the current published version is used.
1060
     * 4.x: The draft is created with the initialLanguage code of the source version or if not present with the main language.
1061
     * It can be changed on updating the version.
1062
     *
1063
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1064
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1065
     * @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
1066
     *
1067
     * @return \eZ\Publish\API\Repository\Values\Content\Content - the newly created content draft
1068
     *
1069
     * @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
1070
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the current-user is not allowed to create the draft
1071
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to create the draft
1072
     */
1073
    public function createContentDraft(ContentInfo $contentInfo, APIVersionInfo $versionInfo = null, User $creator = null)
1074
    {
1075
        $contentInfo = $this->loadContentInfo($contentInfo->id);
1076
1077
        if ($versionInfo !== null) {
1078
            // Check that given $contentInfo and $versionInfo belong to the same content
1079
            if ($versionInfo->getContentInfo()->id != $contentInfo->id) {
1080
                throw new InvalidArgumentException(
1081
                    '$versionInfo',
1082
                    'VersionInfo does not belong to the same content as given ContentInfo'
1083
                );
1084
            }
1085
1086
            $versionInfo = $this->loadVersionInfoById($contentInfo->id, $versionInfo->versionNo);
1087
1088
            switch ($versionInfo->status) {
1089
                case VersionInfo::STATUS_PUBLISHED:
1090
                case VersionInfo::STATUS_ARCHIVED:
1091
                    break;
1092
1093
                default:
1094
                    // @todo: throw an exception here, to be defined
1095
                    throw new BadStateException(
1096
                        '$versionInfo',
1097
                        'Draft can not be created from a draft version'
1098
                    );
1099
            }
1100
1101
            $versionNo = $versionInfo->versionNo;
1102
        } elseif ($contentInfo->published) {
1103
            $versionNo = $contentInfo->currentVersionNo;
1104
        } else {
1105
            // @todo: throw an exception here, to be defined
1106
            throw new BadStateException(
1107
                '$contentInfo',
1108
                'Content is not published, draft can be created only from published or archived version'
1109
            );
1110
        }
1111
1112
        if ($creator === null) {
1113
            $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...
1114
        }
1115
1116
        if (!$this->repository->getPermissionResolver()->canUser(
1117
            'content',
1118
            'edit',
1119
            $contentInfo,
1120
            [
1121
                (new Target\Builder\VersionBuilder())
1122
                    ->changeStatusTo(APIVersionInfo::STATUS_DRAFT)
1123
                    ->build(),
1124
            ]
1125
        )) {
1126
            throw new UnauthorizedException(
1127
                'content',
1128
                'edit',
1129
                ['contentId' => $contentInfo->id]
1130
            );
1131
        }
1132
1133
        $this->repository->beginTransaction();
1134
        try {
1135
            $spiContent = $this->persistenceHandler->contentHandler()->createDraftFromVersion(
1136
                $contentInfo->id,
1137
                $versionNo,
1138
                $creator->getUserId()
1139
            );
1140
            $this->repository->commit();
1141
        } catch (Exception $e) {
1142
            $this->repository->rollback();
1143
            throw $e;
1144
        }
1145
1146
        return $this->domainMapper->buildContentDomainObject(
1147
            $spiContent,
1148
            $this->repository->getContentTypeService()->loadContentType(
1149
                $spiContent->versionInfo->contentInfo->contentTypeId
1150
            )
1151
        );
1152
    }
1153
1154
    /**
1155
     * Loads drafts for a user.
1156
     *
1157
     * If no user is given the drafts for the authenticated user a returned
1158
     *
1159
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the current-user is not allowed to load the draft list
1160
     *
1161
     * @param \eZ\Publish\API\Repository\Values\User\UserReference $user
1162
     *
1163
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo the drafts ({@link VersionInfo}) owned by the given user
1164
     */
1165
    public function loadContentDrafts(User $user = null)
1166
    {
1167
        if ($user === null) {
1168
            $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...
1169
        }
1170
1171
        // throw early if user has absolutely no access to versionread
1172
        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...
1173
            throw new UnauthorizedException('content', 'versionread');
1174
        }
1175
1176
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->loadDraftsForUser($user->getUserId());
1177
        $versionInfoList = [];
1178
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1179
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1180
            // @todo: Change this to filter returned drafts by permissions instead of throwing
1181
            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...
1182
                throw new UnauthorizedException('content', 'versionread', ['contentId' => $versionInfo->contentInfo->id]);
1183
            }
1184
1185
            $versionInfoList[] = $versionInfo;
1186
        }
1187
1188
        return $versionInfoList;
1189
    }
1190
1191
    /**
1192
     * Updates the fields of a draft.
1193
     *
1194
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1195
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1196
     *
1197
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the updated fields
1198
     *
1199
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
1200
     *                                                                               or if a required field is missing / set to an empty value.
1201
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
1202
     *                                                                          or value is set for non-translatable field in language
1203
     *                                                                          other than main.
1204
     *
1205
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
1206
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1207
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
1208
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1209
     */
1210
    public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct)
1211
    {
1212
        $contentUpdateStruct = clone $contentUpdateStruct;
1213
1214
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1215
        $content = $this->loadContent(
1216
            $versionInfo->getContentInfo()->id,
1217
            null,
1218
            $versionInfo->versionNo
1219
        );
1220
        if (!$content->versionInfo->isDraft()) {
1221
            throw new BadStateException(
1222
                '$versionInfo',
1223
                'Version is not a draft and can not be updated'
1224
            );
1225
        }
1226
1227
        if (!$this->repository->getPermissionResolver()->canUser(
1228
            'content',
1229
            'edit',
1230
            $content,
1231
            [
1232
                (new Target\Builder\VersionBuilder())
1233
                    ->updateFieldsTo(
1234
                        $contentUpdateStruct->initialLanguageCode,
1235
                        $contentUpdateStruct->fields
1236
                    )
1237
                    ->build(),
1238
            ]
1239
        )) {
1240
            throw new UnauthorizedException('content', 'edit', ['contentId' => $content->id]);
1241
        }
1242
1243
        $mainLanguageCode = $content->contentInfo->mainLanguageCode;
1244
        if ($contentUpdateStruct->initialLanguageCode === null) {
1245
            $contentUpdateStruct->initialLanguageCode = $mainLanguageCode;
1246
        }
1247
1248
        $allLanguageCodes = $this->getLanguageCodesForUpdate($contentUpdateStruct, $content);
1249
        $contentLanguageHandler = $this->persistenceHandler->contentLanguageHandler();
1250
        foreach ($allLanguageCodes as $languageCode) {
1251
            $contentLanguageHandler->loadByLanguageCode($languageCode);
1252
        }
1253
1254
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1255
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1256
            $content->contentInfo->contentTypeId
1257
        );
1258
        $fields = $this->mapFieldsForUpdate(
1259
            $contentUpdateStruct,
1260
            $contentType,
1261
            $mainLanguageCode
1262
        );
1263
1264
        $fieldValues = [];
1265
        $spiFields = [];
1266
        $allFieldErrors = [];
1267
        $inputRelations = [];
1268
        $locationIdToContentIdMapping = [];
1269
1270
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1271
            /** @var $fieldType \eZ\Publish\SPI\FieldType\FieldType */
1272
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1273
                $fieldDefinition->fieldTypeIdentifier
1274
            );
1275
1276
            foreach ($allLanguageCodes as $languageCode) {
1277
                $isCopied = $isEmpty = $isRetained = false;
1278
                $isLanguageNew = !in_array($languageCode, $content->versionInfo->languageCodes);
1279
                $isLanguageUpdated = in_array($languageCode, $updatedLanguageCodes);
1280
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $mainLanguageCode;
1281
                $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
1282
                $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
1283
1284
                if (!$isFieldUpdated && !$isLanguageNew) {
1285
                    $isRetained = true;
1286
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1287
                } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
1288
                    $isCopied = true;
1289
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1290
                } elseif ($isFieldUpdated) {
1291
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
1292
                } else {
1293
                    $fieldValue = $fieldDefinition->defaultValue;
1294
                }
1295
1296
                $fieldValue = $fieldType->acceptValue($fieldValue);
1297
1298
                if ($fieldType->isEmptyValue($fieldValue)) {
1299
                    $isEmpty = true;
1300
                    if ($isLanguageUpdated && $fieldDefinition->isRequired) {
1301
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
1302
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
1303
                            null,
1304
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
1305
                            'empty'
1306
                        );
1307
                    }
1308
                } elseif ($isLanguageUpdated) {
1309
                    $fieldErrors = $fieldType->validate(
1310
                        $fieldDefinition,
1311
                        $fieldValue
1312
                    );
1313
                    if (!empty($fieldErrors)) {
1314
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
1315
                    }
1316
                }
1317
1318
                if (!empty($allFieldErrors)) {
1319
                    continue;
1320
                }
1321
1322
                $this->relationProcessor->appendFieldRelations(
1323
                    $inputRelations,
1324
                    $locationIdToContentIdMapping,
1325
                    $fieldType,
1326
                    $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...
1327
                    $fieldDefinition->id
1328
                );
1329
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
1330
1331
                if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
1332
                    continue;
1333
                }
1334
1335
                $spiFields[] = new SPIField(
1336
                    [
1337
                        'id' => $isLanguageNew ?
1338
                            null :
1339
                            $content->getField($fieldDefinition->identifier, $languageCode)->id,
1340
                        'fieldDefinitionId' => $fieldDefinition->id,
1341
                        'type' => $fieldDefinition->fieldTypeIdentifier,
1342
                        'value' => $fieldType->toPersistenceValue($fieldValue),
1343
                        'languageCode' => $languageCode,
1344
                        'versionNo' => $versionInfo->versionNo,
1345
                    ]
1346
                );
1347
            }
1348
        }
1349
1350
        if (!empty($allFieldErrors)) {
1351
            throw new ContentFieldValidationException($allFieldErrors);
1352
        }
1353
1354
        $spiContentUpdateStruct = new SPIContentUpdateStruct(
1355
            [
1356
                'name' => $this->nameSchemaService->resolveNameSchema(
1357
                    $content,
1358
                    $fieldValues,
1359
                    $allLanguageCodes,
1360
                    $contentType
1361
                ),
1362
                '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...
1363
                'fields' => $spiFields,
1364
                'modificationDate' => time(),
1365
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1366
                    $contentUpdateStruct->initialLanguageCode
1367
                )->id,
1368
            ]
1369
        );
1370
        $existingRelations = $this->loadRelations($versionInfo);
1371
1372
        $this->repository->beginTransaction();
1373
        try {
1374
            $spiContent = $this->persistenceHandler->contentHandler()->updateContent(
1375
                $versionInfo->getContentInfo()->id,
1376
                $versionInfo->versionNo,
1377
                $spiContentUpdateStruct
1378
            );
1379
            $this->relationProcessor->processFieldRelations(
1380
                $inputRelations,
1381
                $spiContent->versionInfo->contentInfo->id,
1382
                $spiContent->versionInfo->versionNo,
1383
                $contentType,
1384
                $existingRelations
1385
            );
1386
            $this->repository->commit();
1387
        } catch (Exception $e) {
1388
            $this->repository->rollback();
1389
            throw $e;
1390
        }
1391
1392
        return $this->domainMapper->buildContentDomainObject(
1393
            $spiContent,
1394
            $contentType
1395
        );
1396
    }
1397
1398
    /**
1399
     * Returns only updated language codes.
1400
     *
1401
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1402
     *
1403
     * @return array
1404
     */
1405
    private function getUpdatedLanguageCodes(APIContentUpdateStruct $contentUpdateStruct)
1406
    {
1407
        $languageCodes = [
1408
            $contentUpdateStruct->initialLanguageCode => true,
1409
        ];
1410
1411
        foreach ($contentUpdateStruct->fields as $field) {
1412
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
1413
                continue;
1414
            }
1415
1416
            $languageCodes[$field->languageCode] = true;
1417
        }
1418
1419
        return array_keys($languageCodes);
1420
    }
1421
1422
    /**
1423
     * Returns all language codes used in given $fields.
1424
     *
1425
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value exists in initial language
1426
     *
1427
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1428
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1429
     *
1430
     * @return array
1431
     */
1432
    protected function getLanguageCodesForUpdate(APIContentUpdateStruct $contentUpdateStruct, APIContent $content)
1433
    {
1434
        $languageCodes = array_fill_keys($content->versionInfo->languageCodes, true);
1435
        $languageCodes[$contentUpdateStruct->initialLanguageCode] = true;
1436
1437
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1438
        foreach ($updatedLanguageCodes as $languageCode) {
1439
            $languageCodes[$languageCode] = true;
1440
        }
1441
1442
        return array_keys($languageCodes);
1443
    }
1444
1445
    /**
1446
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
1447
     *
1448
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
1449
     *                                                                          or value is set for non-translatable field in language
1450
     *                                                                          other than main
1451
     *
1452
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1453
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1454
     * @param string $mainLanguageCode
1455
     *
1456
     * @return array
1457
     */
1458
    protected function mapFieldsForUpdate(
1459
        APIContentUpdateStruct $contentUpdateStruct,
1460
        ContentType $contentType,
1461
        $mainLanguageCode
1462
    ) {
1463
        $fields = [];
1464
1465
        foreach ($contentUpdateStruct->fields as $field) {
1466
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1467
1468
            if ($fieldDefinition === null) {
1469
                throw new ContentValidationException(
1470
                    "Field definition '%identifier%' does not exist in given ContentType",
1471
                    ['%identifier%' => $field->fieldDefIdentifier]
1472
                );
1473
            }
1474
1475
            if ($field->languageCode === null) {
1476
                if ($fieldDefinition->isTranslatable) {
1477
                    $languageCode = $contentUpdateStruct->initialLanguageCode;
1478
                } else {
1479
                    $languageCode = $mainLanguageCode;
1480
                }
1481
                $field = $this->cloneField($field, ['languageCode' => $languageCode]);
1482
            }
1483
1484
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $mainLanguageCode)) {
1485
                throw new ContentValidationException(
1486
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
1487
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
1488
                );
1489
            }
1490
1491
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
1492
        }
1493
1494
        return $fields;
1495
    }
1496
1497
    /**
1498
     * Publishes a content version.
1499
     *
1500
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1501
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1502
     *
1503
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1504
     * @param string[] $translations
1505
     *
1506
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1507
     *
1508
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1509
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1510
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1511
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1512
     */
1513
    public function publishVersion(APIVersionInfo $versionInfo, array $translations = Language::ALL)
1514
    {
1515
        $content = $this->internalLoadContent(
1516
            $versionInfo->contentInfo->id,
1517
            null,
1518
            $versionInfo->versionNo
1519
        );
1520
1521
        $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...
1522
        if ($content->contentInfo->currentVersionNo !== $versionInfo->versionNo) {
1523
            $fromContent = $this->internalLoadContent(
1524
                $content->contentInfo->id,
1525
                null,
1526
                $content->contentInfo->currentVersionNo
1527
            );
1528
            // should not occur now, might occur in case of un-publish
1529
            if (!$fromContent->contentInfo->isPublished()) {
1530
                $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...
1531
            }
1532
        }
1533
1534
        if (!$this->repository->getPermissionResolver()->canUser(
1535
            'content',
1536
            'publish',
1537
            $content
1538
        )) {
1539
            throw new UnauthorizedException(
1540
                'content', 'publish', ['contentId' => $content->id]
1541
            );
1542
        }
1543
1544
        $this->repository->beginTransaction();
1545
        try {
1546
            $this->copyTranslationsFromPublishedVersion($content->versionInfo, $translations);
1547
            $content = $this->internalPublishVersion($content->getVersionInfo(), null);
1548
            $this->repository->commit();
1549
        } catch (Exception $e) {
1550
            $this->repository->rollback();
1551
            throw $e;
1552
        }
1553
1554
        return $content;
1555
    }
1556
1557
    /**
1558
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1559
     * @param array $translations
1560
     *
1561
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1562
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException
1563
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
1564
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1565
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1566
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1567
     */
1568
    protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionInfo, array $translations = []): void
1569
    {
1570
        $contendId = $versionInfo->contentInfo->id;
1571
1572
        $currentContent = $this->internalLoadContent($contendId);
1573
        $currentVersionInfo = $currentContent->versionInfo;
1574
1575
        // Copying occurs only if:
1576
        // - There is published Version
1577
        // - Published version is older than the currently published one unless specific translations are provided.
1578
        if (!$currentVersionInfo->isPublished() ||
1579
            ($versionInfo->versionNo >= $currentVersionInfo->versionNo && empty($translations))) {
1580
            return;
1581
        }
1582
1583
        if (empty($translations)) {
1584
            $languagesToCopy = array_diff(
1585
                $currentVersionInfo->languageCodes,
1586
                $versionInfo->languageCodes
1587
            );
1588
        } else {
1589
            $languagesToCopy = array_diff(
1590
                $currentVersionInfo->languageCodes,
1591
                $translations
1592
            );
1593
        }
1594
1595
        if (empty($languagesToCopy)) {
1596
            return;
1597
        }
1598
1599
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1600
            $currentVersionInfo->contentInfo->contentTypeId
1601
        );
1602
1603
        // Find only translatable fields to update with selected languages
1604
        $updateStruct = $this->newContentUpdateStruct();
1605
        $updateStruct->initialLanguageCode = $versionInfo->initialLanguageCode;
1606
1607
        foreach ($currentContent->getFields() as $field) {
1608
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1609
1610
            if ($fieldDefinition->isTranslatable && in_array($field->languageCode, $languagesToCopy)) {
1611
                $updateStruct->setField($field->fieldDefIdentifier, $field->value, $field->languageCode);
1612
            }
1613
        }
1614
1615
        $this->updateContent($versionInfo, $updateStruct);
1616
    }
1617
1618
    /**
1619
     * Publishes a content version.
1620
     *
1621
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1622
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1623
     *
1624
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1625
     *
1626
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1627
     * @param int|null $publicationDate If null existing date is kept if there is one, otherwise current time is used.
1628
     *
1629
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1630
     */
1631
    protected function internalPublishVersion(APIVersionInfo $versionInfo, $publicationDate = null)
1632
    {
1633
        if (!$versionInfo->isDraft()) {
1634
            throw new BadStateException('$versionInfo', 'Only versions in draft status can be published.');
1635
        }
1636
1637
        $currentTime = $this->getUnixTimestamp();
1638
        if ($publicationDate === null && $versionInfo->versionNo === 1) {
1639
            $publicationDate = $currentTime;
1640
        }
1641
1642
        $metadataUpdateStruct = new SPIMetadataUpdateStruct();
1643
        $metadataUpdateStruct->publicationDate = $publicationDate;
1644
        $metadataUpdateStruct->modificationDate = $currentTime;
1645
1646
        $contentId = $versionInfo->getContentInfo()->id;
1647
        $spiContent = $this->persistenceHandler->contentHandler()->publish(
1648
            $contentId,
1649
            $versionInfo->versionNo,
1650
            $metadataUpdateStruct
1651
        );
1652
1653
        $content = $this->domainMapper->buildContentDomainObject(
1654
            $spiContent,
1655
            $this->repository->getContentTypeService()->loadContentType(
1656
                $spiContent->versionInfo->contentInfo->contentTypeId
1657
            )
1658
        );
1659
1660
        $this->publishUrlAliasesForContent($content);
1661
1662
        // Delete version archive overflow if any, limit is 0-50 (however 0 will mean 1 if content is unpublished)
1663
        $archiveList = $this->persistenceHandler->contentHandler()->listVersions(
1664
            $contentId,
1665
            APIVersionInfo::STATUS_ARCHIVED,
1666
            100 // Limited to avoid publishing taking to long, besides SE limitations this is why limit is max 50
1667
        );
1668
1669
        $maxVersionArchiveCount = max(0, min(50, $this->settings['default_version_archive_limit']));
1670
        while (!empty($archiveList) && count($archiveList) > $maxVersionArchiveCount) {
1671
            /** @var \eZ\Publish\SPI\Persistence\Content\VersionInfo $archiveVersion */
1672
            $archiveVersion = array_shift($archiveList);
1673
            $this->persistenceHandler->contentHandler()->deleteVersion(
1674
                $contentId,
1675
                $archiveVersion->versionNo
1676
            );
1677
        }
1678
1679
        return $content;
1680
    }
1681
1682
    /**
1683
     * @return int
1684
     */
1685
    protected function getUnixTimestamp()
1686
    {
1687
        return time();
1688
    }
1689
1690
    /**
1691
     * Removes the given version.
1692
     *
1693
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is in
1694
     *         published state or is a last version of Content in non draft state
1695
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to remove this version
1696
     *
1697
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1698
     */
1699
    public function deleteVersion(APIVersionInfo $versionInfo)
1700
    {
1701
        if ($versionInfo->isPublished()) {
1702
            throw new BadStateException(
1703
                '$versionInfo',
1704
                'Version is published and can not be removed'
1705
            );
1706
        }
1707
1708
        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...
1709
            throw new UnauthorizedException(
1710
                'content',
1711
                'versionremove',
1712
                ['contentId' => $versionInfo->contentInfo->id, 'versionNo' => $versionInfo->versionNo]
1713
            );
1714
        }
1715
1716
        $versionList = $this->persistenceHandler->contentHandler()->listVersions(
1717
            $versionInfo->contentInfo->id,
1718
            null,
1719
            2
1720
        );
1721
1722
        if (count($versionList) === 1 && !$versionInfo->isDraft()) {
1723
            throw new BadStateException(
1724
                '$versionInfo',
1725
                'Version is the last version of the Content and can not be removed'
1726
            );
1727
        }
1728
1729
        $this->repository->beginTransaction();
1730
        try {
1731
            $this->persistenceHandler->contentHandler()->deleteVersion(
1732
                $versionInfo->getContentInfo()->id,
1733
                $versionInfo->versionNo
1734
            );
1735
            $this->repository->commit();
1736
        } catch (Exception $e) {
1737
            $this->repository->rollback();
1738
            throw $e;
1739
        }
1740
    }
1741
1742
    /**
1743
     * Loads all versions for the given content.
1744
     *
1745
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to list versions
1746
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the given status is invalid
1747
     *
1748
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1749
     * @param int|null $status
1750
     *
1751
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Sorted by creation date
1752
     */
1753
    public function loadVersions(ContentInfo $contentInfo, ?int $status = null)
1754
    {
1755
        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...
1756
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentInfo->id]);
1757
        }
1758
1759
        if ($status !== null && !in_array((int)$status, [VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED], true)) {
1760
            throw new InvalidArgumentException(
1761
                'status',
1762
                sprintf(
1763
                    'it can be one of %d (draft), %d (published), %d (archived), %d given',
1764
                    VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED, $status
1765
                ));
1766
        }
1767
1768
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->listVersions($contentInfo->id, $status);
1769
1770
        $versions = [];
1771
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1772
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1773
            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...
1774
                throw new UnauthorizedException('content', 'versionread', ['versionId' => $versionInfo->id]);
1775
            }
1776
1777
            $versions[] = $versionInfo;
1778
        }
1779
1780
        return $versions;
1781
    }
1782
1783
    /**
1784
     * Copies the content to a new location. If no version is given,
1785
     * all versions are copied, otherwise only the given version.
1786
     *
1787
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to copy the content to the given location
1788
     *
1789
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1790
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $destinationLocationCreateStruct the target location where the content is copied to
1791
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1792
     *
1793
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1794
     */
1795
    public function copyContent(ContentInfo $contentInfo, LocationCreateStruct $destinationLocationCreateStruct, APIVersionInfo $versionInfo = null)
1796
    {
1797
        $destinationLocation = $this->repository->getLocationService()->loadLocation(
1798
            $destinationLocationCreateStruct->parentLocationId
1799
        );
1800
        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...
1801
            throw new UnauthorizedException(
1802
                'content',
1803
                'create',
1804
                [
1805
                    'parentLocationId' => $destinationLocationCreateStruct->parentLocationId,
1806
                    'sectionId' => $contentInfo->sectionId,
1807
                ]
1808
            );
1809
        }
1810
        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...
1811
            throw new UnauthorizedException('content', 'manage_locations', ['contentId' => $contentInfo->id]);
1812
        }
1813
1814
        $defaultObjectStates = $this->getDefaultObjectStates();
1815
1816
        $this->repository->beginTransaction();
1817
        try {
1818
            $spiContent = $this->persistenceHandler->contentHandler()->copy(
1819
                $contentInfo->id,
1820
                $versionInfo ? $versionInfo->versionNo : null,
1821
                $this->repository->getPermissionResolver()->getCurrentUserReference()->getUserId()
1822
            );
1823
1824
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
1825
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
1826
                $objectStateHandler->setContentState(
1827
                    $spiContent->versionInfo->contentInfo->id,
1828
                    $objectStateGroupId,
1829
                    $objectState->id
1830
                );
1831
            }
1832
1833
            $content = $this->internalPublishVersion(
1834
                $this->domainMapper->buildVersionInfoDomainObject($spiContent->versionInfo),
1835
                $spiContent->versionInfo->creationDate
1836
            );
1837
1838
            $this->repository->getLocationService()->createLocation(
1839
                $content->getVersionInfo()->getContentInfo(),
1840
                $destinationLocationCreateStruct
1841
            );
1842
            $this->repository->commit();
1843
        } catch (Exception $e) {
1844
            $this->repository->rollback();
1845
            throw $e;
1846
        }
1847
1848
        return $this->internalLoadContent($content->id);
1849
    }
1850
1851
    /**
1852
     * Loads all outgoing relations for the given version.
1853
     *
1854
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1855
     *
1856
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1857
     *
1858
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1859
     */
1860
    public function loadRelations(APIVersionInfo $versionInfo)
1861
    {
1862
        if ($versionInfo->isPublished()) {
1863
            $function = 'read';
1864
        } else {
1865
            $function = 'versionread';
1866
        }
1867
1868
        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...
1869
            throw new UnauthorizedException('content', $function);
1870
        }
1871
1872
        $contentInfo = $versionInfo->getContentInfo();
1873
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
1874
            $contentInfo->id,
1875
            $versionInfo->versionNo
1876
        );
1877
1878
        /** @var $relations \eZ\Publish\API\Repository\Values\Content\Relation[] */
1879
        $relations = [];
1880
        foreach ($spiRelations as $spiRelation) {
1881
            $destinationContentInfo = $this->internalLoadContentInfo($spiRelation->destinationContentId);
1882
            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...
1883
                continue;
1884
            }
1885
1886
            $relations[] = $this->domainMapper->buildRelationDomainObject(
1887
                $spiRelation,
1888
                $contentInfo,
1889
                $destinationContentInfo
1890
            );
1891
        }
1892
1893
        return $relations;
1894
    }
1895
1896
    /**
1897
     * Loads all incoming relations for a content object.
1898
     *
1899
     * The relations come only from published versions of the source content objects
1900
     *
1901
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1902
     *
1903
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1904
     *
1905
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1906
     */
1907
    public function loadReverseRelations(ContentInfo $contentInfo)
1908
    {
1909
        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...
1910
            throw new UnauthorizedException('content', 'reverserelatedlist', ['contentId' => $contentInfo->id]);
1911
        }
1912
1913
        $spiRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations(
1914
            $contentInfo->id
1915
        );
1916
1917
        $returnArray = [];
1918
        foreach ($spiRelations as $spiRelation) {
1919
            $sourceContentInfo = $this->internalLoadContentInfo($spiRelation->sourceContentId);
1920
            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...
1921
                continue;
1922
            }
1923
1924
            $returnArray[] = $this->domainMapper->buildRelationDomainObject(
1925
                $spiRelation,
1926
                $sourceContentInfo,
1927
                $contentInfo
1928
            );
1929
        }
1930
1931
        return $returnArray;
1932
    }
1933
1934
    /**
1935
     * {@inheritdoc}
1936
     */
1937
    public function loadReverseRelationList(ContentInfo $contentInfo, int $offset = 0, int $limit = -1): RelationList
1938
    {
1939
        $list = new RelationList();
1940
        if (!$this->repository->getPermissionResolver()->canUser('content', 'reverserelatedlist', $contentInfo)) {
1941
            return $list;
1942
        }
1943
1944
        $list->totalCount = $this->persistenceHandler->contentHandler()->countReverseRelations(
0 ignored issues
show
Bug introduced by
The method countReverseRelations() does not seem to exist on object<eZ\Publish\SPI\Pe...stence\Content\Handler>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1945
            $contentInfo->id
1946
        );
1947
        if ($list->totalCount > 0) {
1948
            $spiRelationList = $this->persistenceHandler->contentHandler()->loadReverseRelationList(
1949
                $contentInfo->id,
1950
                $offset,
1951
                $limit
1952
            );
1953
            foreach ($spiRelationList as $spiRelation) {
1954
                $sourceContentInfo = $this->internalLoadContentInfo($spiRelation->sourceContentId);
1955
                if ($this->repository->getPermissionResolver()->canUser('content', 'read', $sourceContentInfo)) {
1956
                    $relation = $this->domainMapper->buildRelationDomainObject(
1957
                        $spiRelation,
1958
                        $sourceContentInfo,
1959
                        $contentInfo
1960
                    );
1961
                    $list->items[] = new RelationListItem($relation);
1962
                } else {
1963
                    $list->items[] = new UnauthorizedRelationListItem(
1964
                        'content',
1965
                        'read',
1966
                        ['contentId' => $sourceContentInfo->id]
1967
                    );
1968
                }
1969
            }
1970
        }
1971
1972
        return $list;
1973
    }
1974
1975
    /**
1976
     * Adds a relation of type common.
1977
     *
1978
     * The source of the relation is the content and version
1979
     * referenced by $versionInfo.
1980
     *
1981
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to edit this version
1982
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1983
     *
1984
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
1985
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent the destination of the relation
1986
     *
1987
     * @return \eZ\Publish\API\Repository\Values\Content\Relation the newly created relation
1988
     */
1989
    public function addRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
1990
    {
1991
        $sourceVersion = $this->loadVersionInfoById(
1992
            $sourceVersion->contentInfo->id,
1993
            $sourceVersion->versionNo
1994
        );
1995
1996
        if (!$sourceVersion->isDraft()) {
1997
            throw new BadStateException(
1998
                '$sourceVersion',
1999
                'Relations of type common can only be added to versions of status draft'
2000
            );
2001
        }
2002
2003
        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...
2004
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2005
        }
2006
2007
        $sourceContentInfo = $sourceVersion->getContentInfo();
2008
2009
        $this->repository->beginTransaction();
2010
        try {
2011
            $spiRelation = $this->persistenceHandler->contentHandler()->addRelation(
2012
                new SPIRelationCreateStruct(
2013
                    [
2014
                        'sourceContentId' => $sourceContentInfo->id,
2015
                        'sourceContentVersionNo' => $sourceVersion->versionNo,
2016
                        'sourceFieldDefinitionId' => null,
2017
                        'destinationContentId' => $destinationContent->id,
2018
                        'type' => APIRelation::COMMON,
2019
                    ]
2020
                )
2021
            );
2022
            $this->repository->commit();
2023
        } catch (Exception $e) {
2024
            $this->repository->rollback();
2025
            throw $e;
2026
        }
2027
2028
        return $this->domainMapper->buildRelationDomainObject($spiRelation, $sourceContentInfo, $destinationContent);
2029
    }
2030
2031
    /**
2032
     * Removes a relation of type COMMON from a draft.
2033
     *
2034
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed edit this version
2035
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2036
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is no relation of type COMMON for the given destination
2037
     *
2038
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2039
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent
2040
     */
2041
    public function deleteRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
2042
    {
2043
        $sourceVersion = $this->loadVersionInfoById(
2044
            $sourceVersion->contentInfo->id,
2045
            $sourceVersion->versionNo
2046
        );
2047
2048
        if (!$sourceVersion->isDraft()) {
2049
            throw new BadStateException(
2050
                '$sourceVersion',
2051
                'Relations of type common can only be removed from versions of status draft'
2052
            );
2053
        }
2054
2055
        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...
2056
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2057
        }
2058
2059
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
2060
            $sourceVersion->getContentInfo()->id,
2061
            $sourceVersion->versionNo,
2062
            APIRelation::COMMON
2063
        );
2064
2065
        if (empty($spiRelations)) {
2066
            throw new InvalidArgumentException(
2067
                '$sourceVersion',
2068
                'There are no relations of type COMMON for the given destination'
2069
            );
2070
        }
2071
2072
        // there should be only one relation of type COMMON for each destination,
2073
        // but in case there were ever more then one, we will remove them all
2074
        // @todo: alternatively, throw BadStateException?
2075
        $this->repository->beginTransaction();
2076
        try {
2077
            foreach ($spiRelations as $spiRelation) {
2078
                if ($spiRelation->destinationContentId == $destinationContent->id) {
2079
                    $this->persistenceHandler->contentHandler()->removeRelation(
2080
                        $spiRelation->id,
2081
                        APIRelation::COMMON
2082
                    );
2083
                }
2084
            }
2085
            $this->repository->commit();
2086
        } catch (Exception $e) {
2087
            $this->repository->rollback();
2088
            throw $e;
2089
        }
2090
    }
2091
2092
    /**
2093
     * {@inheritdoc}
2094
     */
2095
    public function removeTranslation(ContentInfo $contentInfo, $languageCode)
2096
    {
2097
        @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...
2098
            __METHOD__ . ' is deprecated, use deleteTranslation instead',
2099
            E_USER_DEPRECATED
2100
        );
2101
        $this->deleteTranslation($contentInfo, $languageCode);
2102
    }
2103
2104
    /**
2105
     * Delete Content item Translation from all Versions (including archived ones) of a Content Object.
2106
     *
2107
     * NOTE: this operation is risky and permanent, so user interface should provide a warning before performing it.
2108
     *
2109
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2110
     *         is the Main Translation of a Content Item.
2111
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2112
     *         to delete the content (in one of the locations of the given Content Item).
2113
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2114
     *         is invalid for the given content.
2115
     *
2116
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2117
     * @param string $languageCode
2118
     *
2119
     * @since 6.13
2120
     */
2121
    public function deleteTranslation(ContentInfo $contentInfo, $languageCode)
2122
    {
2123
        if ($contentInfo->mainLanguageCode === $languageCode) {
2124
            throw new BadStateException(
2125
                '$languageCode',
2126
                'Specified translation is the main translation of the Content Object'
2127
            );
2128
        }
2129
2130
        $translationWasFound = false;
2131
        $this->repository->beginTransaction();
2132
        try {
2133
            foreach ($this->loadVersions($contentInfo) as $versionInfo) {
2134
                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...
2135
                    throw new UnauthorizedException(
2136
                        'content',
2137
                        'remove',
2138
                        ['contentId' => $contentInfo->id, 'versionNo' => $versionInfo->versionNo]
2139
                    );
2140
                }
2141
2142
                if (!in_array($languageCode, $versionInfo->languageCodes)) {
2143
                    continue;
2144
                }
2145
2146
                $translationWasFound = true;
2147
2148
                // If the translation is the version's only one, delete the version
2149
                if (count($versionInfo->languageCodes) < 2) {
2150
                    $this->persistenceHandler->contentHandler()->deleteVersion(
2151
                        $versionInfo->getContentInfo()->id,
2152
                        $versionInfo->versionNo
2153
                    );
2154
                }
2155
            }
2156
2157
            if (!$translationWasFound) {
2158
                throw new InvalidArgumentException(
2159
                    '$languageCode',
2160
                    sprintf(
2161
                        '%s does not exist in the Content item(id=%d)',
2162
                        $languageCode,
2163
                        $contentInfo->id
2164
                    )
2165
                );
2166
            }
2167
2168
            $this->persistenceHandler->contentHandler()->deleteTranslationFromContent(
2169
                $contentInfo->id,
2170
                $languageCode
2171
            );
2172
            $locationIds = array_map(
2173
                function (Location $location) {
2174
                    return $location->id;
2175
                },
2176
                $this->repository->getLocationService()->loadLocations($contentInfo)
2177
            );
2178
            $this->persistenceHandler->urlAliasHandler()->translationRemoved(
2179
                $locationIds,
2180
                $languageCode
2181
            );
2182
            $this->repository->commit();
2183
        } catch (InvalidArgumentException $e) {
2184
            $this->repository->rollback();
2185
            throw $e;
2186
        } catch (BadStateException $e) {
2187
            $this->repository->rollback();
2188
            throw $e;
2189
        } catch (UnauthorizedException $e) {
2190
            $this->repository->rollback();
2191
            throw $e;
2192
        } catch (Exception $e) {
2193
            $this->repository->rollback();
2194
            // cover generic unexpected exception to fulfill API promise on @throws
2195
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2196
        }
2197
    }
2198
2199
    /**
2200
     * Delete specified Translation from a Content Draft.
2201
     *
2202
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2203
     *         is the only one the Content Draft has or it is the main Translation of a Content Object.
2204
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2205
     *         to edit the Content (in one of the locations of the given Content Object).
2206
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2207
     *         is invalid for the given Draft.
2208
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if specified Version was not found
2209
     *
2210
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo Content Version Draft
2211
     * @param string $languageCode Language code of the Translation to be removed
2212
     *
2213
     * @return \eZ\Publish\API\Repository\Values\Content\Content Content Draft w/o the specified Translation
2214
     *
2215
     * @since 6.12
2216
     */
2217
    public function deleteTranslationFromDraft(APIVersionInfo $versionInfo, $languageCode)
2218
    {
2219
        if (!$versionInfo->isDraft()) {
2220
            throw new BadStateException(
2221
                '$versionInfo',
2222
                'Version is not a draft, so Translations cannot be modified. Create a Draft before proceeding'
2223
            );
2224
        }
2225
2226
        if ($versionInfo->contentInfo->mainLanguageCode === $languageCode) {
2227
            throw new BadStateException(
2228
                '$languageCode',
2229
                'Specified Translation is the main Translation of the Content Object. Change it before proceeding.'
2230
            );
2231
        }
2232
2233
        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...
2234
            throw new UnauthorizedException(
2235
                'content', 'edit', ['contentId' => $versionInfo->contentInfo->id]
2236
            );
2237
        }
2238
2239
        if (!in_array($languageCode, $versionInfo->languageCodes)) {
2240
            throw new InvalidArgumentException(
2241
                '$languageCode',
2242
                sprintf(
2243
                    'The Version (ContentId=%d, VersionNo=%d) is not translated into %s',
2244
                    $versionInfo->contentInfo->id,
2245
                    $versionInfo->versionNo,
2246
                    $languageCode
2247
                )
2248
            );
2249
        }
2250
2251
        if (count($versionInfo->languageCodes) === 1) {
2252
            throw new BadStateException(
2253
                '$languageCode',
2254
                'Specified Translation is the only one Content Object Version has'
2255
            );
2256
        }
2257
2258
        $this->repository->beginTransaction();
2259
        try {
2260
            $spiContent = $this->persistenceHandler->contentHandler()->deleteTranslationFromDraft(
2261
                $versionInfo->contentInfo->id,
2262
                $versionInfo->versionNo,
2263
                $languageCode
2264
            );
2265
            $this->repository->commit();
2266
2267
            return $this->domainMapper->buildContentDomainObject(
2268
                $spiContent,
2269
                $this->repository->getContentTypeService()->loadContentType(
2270
                    $spiContent->versionInfo->contentInfo->contentTypeId
2271
                )
2272
            );
2273
        } catch (APINotFoundException $e) {
2274
            // avoid wrapping expected NotFoundException in BadStateException handled below
2275
            $this->repository->rollback();
2276
            throw $e;
2277
        } catch (Exception $e) {
2278
            $this->repository->rollback();
2279
            // cover generic unexpected exception to fulfill API promise on @throws
2280
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2281
        }
2282
    }
2283
2284
    /**
2285
     * Hides Content by making all the Locations appear hidden.
2286
     * It does not persist hidden state on Location object itself.
2287
     *
2288
     * Content hidden by this API can be revealed by revealContent API.
2289
     *
2290
     * @see revealContent
2291
     *
2292
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2293
     */
2294
    public function hideContent(ContentInfo $contentInfo): void
2295
    {
2296
        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...
2297
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2298
        }
2299
2300
        $this->repository->beginTransaction();
2301
        try {
2302
            $this->persistenceHandler->contentHandler()->updateMetadata(
2303
                $contentInfo->id,
2304
                new SPIMetadataUpdateStruct([
2305
                    'isHidden' => true,
2306
                ])
2307
            );
2308
            $locationHandler = $this->persistenceHandler->locationHandler();
2309
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2310
            foreach ($childLocations as $childLocation) {
2311
                $locationHandler->setInvisible($childLocation->id);
2312
            }
2313
            $this->repository->commit();
2314
        } catch (Exception $e) {
2315
            $this->repository->rollback();
2316
            throw $e;
2317
        }
2318
    }
2319
2320
    /**
2321
     * Reveals Content hidden by hideContent API.
2322
     * Locations which were hidden before hiding Content will remain hidden.
2323
     *
2324
     * @see hideContent
2325
     *
2326
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2327
     */
2328
    public function revealContent(ContentInfo $contentInfo): void
2329
    {
2330
        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...
2331
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2332
        }
2333
2334
        $this->repository->beginTransaction();
2335
        try {
2336
            $this->persistenceHandler->contentHandler()->updateMetadata(
2337
                $contentInfo->id,
2338
                new SPIMetadataUpdateStruct([
2339
                    'isHidden' => false,
2340
                ])
2341
            );
2342
            $locationHandler = $this->persistenceHandler->locationHandler();
2343
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2344
            foreach ($childLocations as $childLocation) {
2345
                $locationHandler->setVisible($childLocation->id);
2346
            }
2347
            $this->repository->commit();
2348
        } catch (Exception $e) {
2349
            $this->repository->rollback();
2350
            throw $e;
2351
        }
2352
    }
2353
2354
    /**
2355
     * Instantiates a new content create struct object.
2356
     *
2357
     * alwaysAvailable is set to the ContentType's defaultAlwaysAvailable
2358
     *
2359
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
2360
     * @param string $mainLanguageCode
2361
     *
2362
     * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
2363
     */
2364
    public function newContentCreateStruct(ContentType $contentType, $mainLanguageCode)
2365
    {
2366
        return new ContentCreateStruct(
2367
            [
2368
                'contentType' => $contentType,
2369
                'mainLanguageCode' => $mainLanguageCode,
2370
                'alwaysAvailable' => $contentType->defaultAlwaysAvailable,
2371
            ]
2372
        );
2373
    }
2374
2375
    /**
2376
     * Instantiates a new content meta data update struct.
2377
     *
2378
     * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
2379
     */
2380
    public function newContentMetadataUpdateStruct()
2381
    {
2382
        return new ContentMetadataUpdateStruct();
2383
    }
2384
2385
    /**
2386
     * Instantiates a new content update struct.
2387
     *
2388
     * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
2389
     */
2390
    public function newContentUpdateStruct()
2391
    {
2392
        return new ContentUpdateStruct();
2393
    }
2394
}
2395