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