Completed
Push — EZP-30427 ( 8c1757...93ffe2 )
by
unknown
17:19
created

ContentService::loadContentDraftList()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

class Alien {}

class Dalek extends Alien {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
320
        ) {
321
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentId, 'versionNo' => $versionNo]);
322
        }
323
324
        return $content;
325
    }
326
327
    /**
328
     * Loads content in a version of the given content object.
329
     *
330
     * If no version number is given, the method returns the current version
331
     *
332
     * @internal
333
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the content or version with the given id and languages does not exist
334
     *
335
     * @param mixed $id
336
     * @param array|null $languages A language priority, filters returned fields and is used as prioritized language code on
337
     *                         returned value object. If not given all languages are returned.
338
     * @param int|null $versionNo the version number. If not given the current version is returned
339
     * @param bool $isRemoteId
340
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
341
     *
342
     * @return \eZ\Publish\API\Repository\Values\Content\Content
343
     */
344
    public function internalLoadContent($id, array $languages = null, $versionNo = null, $isRemoteId = false, $useAlwaysAvailable = true)
345
    {
346
        try {
347
            // Get Content ID if lookup by remote ID
348
            if ($isRemoteId) {
349
                $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfoByRemoteId($id);
350
                $id = $spiContentInfo->id;
351
                // Set $isRemoteId to false as the next loads will be for content id now that we have it (for exception use now)
352
                $isRemoteId = false;
353
            }
354
355
            $loadLanguages = $languages;
356
            $alwaysAvailableLanguageCode = null;
357
            // Set main language on $languages filter if not empty (all) and $useAlwaysAvailable being true
358
            // @todo Move use always available logic to SPI load methods, like done in location handler in 7.x
359
            if (!empty($loadLanguages) && $useAlwaysAvailable) {
360
                if (!isset($spiContentInfo)) {
361
                    $spiContentInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($id);
362
                }
363
364
                if ($spiContentInfo->alwaysAvailable) {
365
                    $loadLanguages[] = $alwaysAvailableLanguageCode = $spiContentInfo->mainLanguageCode;
366
                    $loadLanguages = array_unique($loadLanguages);
367
                }
368
            }
369
370
            $spiContent = $this->persistenceHandler->contentHandler()->load(
371
                $id,
372
                $versionNo,
373
                $loadLanguages
0 ignored issues
show
Bug introduced by
It seems like $loadLanguages defined by $languages on line 355 can also be of type array; however, eZ\Publish\SPI\Persistence\Content\Handler::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
374
            );
375
        } catch (APINotFoundException $e) {
376
            throw new NotFoundException(
377
                'Content',
378
                [
379
                    $isRemoteId ? 'remoteId' : 'id' => $id,
380
                    'languages' => $languages,
381
                    'versionNo' => $versionNo,
382
                ],
383
                $e
384
            );
385
        }
386
387
        return $this->domainMapper->buildContentDomainObject(
388
            $spiContent,
389
            $this->repository->getContentTypeService()->loadContentType(
390
                $spiContent->versionInfo->contentInfo->contentTypeId
391
            ),
392
            $languages ?? [],
393
            $alwaysAvailableLanguageCode
394
        );
395
    }
396
397
    /**
398
     * Loads content in a version for the content object reference by the given remote id.
399
     *
400
     * If no version is given, the method returns the current version
401
     *
402
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the content or version with the given remote id does not exist
403
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the user has no access to read content and in case of un-published content: read versions
404
     *
405
     * @param string $remoteId
406
     * @param array $languages A language filter for fields. If not given all languages are returned
407
     * @param int $versionNo the version number. If not given the current version is returned
408
     * @param bool $useAlwaysAvailable Add Main language to \$languages if true (default) and if alwaysAvailable is true
409
     *
410
     * @return \eZ\Publish\API\Repository\Values\Content\Content
411
     */
412
    public function loadContentByRemoteId($remoteId, array $languages = null, $versionNo = null, $useAlwaysAvailable = true)
413
    {
414
        $content = $this->internalLoadContent($remoteId, $languages, $versionNo, true, $useAlwaysAvailable);
415
416
        if (!$this->repository->canUser('content', 'read', $content)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
1235
                    $contentDraftList[] = new UnauthorizedContentDraftListItem(
1236
                        'content',
1237
                        'versionread',
1238
                        ['contentId' => $versionInfo->contentInfo->id]
1239
                    );
1240
                    continue;
1241
                }
1242
1243
                $contentDraftList[] = new ContentDraftListItem($versionInfo);
1244
            }
1245
1246
            $list->items = $contentDraftList;
1247
        }
1248
1249
        return $list;
1250
    }
1251
1252
    /**
1253
     * Updates the fields of a draft.
1254
     *
1255
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1256
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1257
     *
1258
     * @return \eZ\Publish\API\Repository\Values\Content\Content the content draft with the updated fields
1259
     *
1260
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
1261
     *                                                                               or if a required field is missing / set to an empty value.
1262
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
1263
     *                                                                          or value is set for non-translatable field in language
1264
     *                                                                          other than main.
1265
     *
1266
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to update this version
1267
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1268
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
1269
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1270
     */
1271
    public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct)
1272
    {
1273
        $contentUpdateStruct = clone $contentUpdateStruct;
1274
1275
        /** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
1276
        $content = $this->loadContent(
1277
            $versionInfo->getContentInfo()->id,
1278
            null,
1279
            $versionInfo->versionNo
1280
        );
1281
        if (!$content->versionInfo->isDraft()) {
1282
            throw new BadStateException(
1283
                '$versionInfo',
1284
                'Version is not a draft and can not be updated'
1285
            );
1286
        }
1287
1288
        if (!$this->repository->getPermissionResolver()->canUser(
1289
            'content',
1290
            'edit',
1291
            $content,
1292
            [
1293
                (new Target\Builder\VersionBuilder())
1294
                    ->updateFieldsTo(
1295
                        $contentUpdateStruct->initialLanguageCode,
1296
                        $contentUpdateStruct->fields
1297
                    )
1298
                    ->build(),
1299
            ]
1300
        )) {
1301
            throw new UnauthorizedException('content', 'edit', ['contentId' => $content->id]);
1302
        }
1303
1304
        $mainLanguageCode = $content->contentInfo->mainLanguageCode;
1305
        if ($contentUpdateStruct->initialLanguageCode === null) {
1306
            $contentUpdateStruct->initialLanguageCode = $mainLanguageCode;
1307
        }
1308
1309
        $allLanguageCodes = $this->getLanguageCodesForUpdate($contentUpdateStruct, $content);
1310
        $contentLanguageHandler = $this->persistenceHandler->contentLanguageHandler();
1311
        foreach ($allLanguageCodes as $languageCode) {
1312
            $contentLanguageHandler->loadByLanguageCode($languageCode);
1313
        }
1314
1315
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1316
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1317
            $content->contentInfo->contentTypeId
1318
        );
1319
        $fields = $this->mapFieldsForUpdate(
1320
            $contentUpdateStruct,
1321
            $contentType,
1322
            $mainLanguageCode
1323
        );
1324
1325
        $fieldValues = [];
1326
        $spiFields = [];
1327
        $allFieldErrors = [];
1328
        $inputRelations = [];
1329
        $locationIdToContentIdMapping = [];
1330
1331
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1332
            /** @var $fieldType \eZ\Publish\SPI\FieldType\FieldType */
1333
            $fieldType = $this->fieldTypeRegistry->getFieldType(
1334
                $fieldDefinition->fieldTypeIdentifier
1335
            );
1336
1337
            foreach ($allLanguageCodes as $languageCode) {
1338
                $isCopied = $isEmpty = $isRetained = false;
1339
                $isLanguageNew = !in_array($languageCode, $content->versionInfo->languageCodes);
1340
                $isLanguageUpdated = in_array($languageCode, $updatedLanguageCodes);
1341
                $valueLanguageCode = $fieldDefinition->isTranslatable ? $languageCode : $mainLanguageCode;
1342
                $isFieldUpdated = isset($fields[$fieldDefinition->identifier][$valueLanguageCode]);
1343
                $isProcessed = isset($fieldValues[$fieldDefinition->identifier][$valueLanguageCode]);
1344
1345
                if (!$isFieldUpdated && !$isLanguageNew) {
1346
                    $isRetained = true;
1347
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1348
                } elseif (!$isFieldUpdated && $isLanguageNew && !$fieldDefinition->isTranslatable) {
1349
                    $isCopied = true;
1350
                    $fieldValue = $content->getField($fieldDefinition->identifier, $valueLanguageCode)->value;
1351
                } elseif ($isFieldUpdated) {
1352
                    $fieldValue = $fields[$fieldDefinition->identifier][$valueLanguageCode]->value;
1353
                } else {
1354
                    $fieldValue = $fieldDefinition->defaultValue;
1355
                }
1356
1357
                $fieldValue = $fieldType->acceptValue($fieldValue);
1358
1359
                if ($fieldType->isEmptyValue($fieldValue)) {
1360
                    $isEmpty = true;
1361
                    if ($isLanguageUpdated && $fieldDefinition->isRequired) {
1362
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = new ValidationError(
1363
                            "Value for required field definition '%identifier%' with language '%languageCode%' is empty",
1364
                            null,
1365
                            ['%identifier%' => $fieldDefinition->identifier, '%languageCode%' => $languageCode],
1366
                            'empty'
1367
                        );
1368
                    }
1369
                } elseif ($isLanguageUpdated) {
1370
                    $fieldErrors = $fieldType->validate(
1371
                        $fieldDefinition,
1372
                        $fieldValue
1373
                    );
1374
                    if (!empty($fieldErrors)) {
1375
                        $allFieldErrors[$fieldDefinition->id][$languageCode] = $fieldErrors;
1376
                    }
1377
                }
1378
1379
                if (!empty($allFieldErrors)) {
1380
                    continue;
1381
                }
1382
1383
                $this->relationProcessor->appendFieldRelations(
1384
                    $inputRelations,
1385
                    $locationIdToContentIdMapping,
1386
                    $fieldType,
1387
                    $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...
1388
                    $fieldDefinition->id
1389
                );
1390
                $fieldValues[$fieldDefinition->identifier][$languageCode] = $fieldValue;
1391
1392
                if ($isRetained || $isCopied || ($isLanguageNew && $isEmpty) || $isProcessed) {
1393
                    continue;
1394
                }
1395
1396
                $spiFields[] = new SPIField(
1397
                    [
1398
                        'id' => $isLanguageNew ?
1399
                            null :
1400
                            $content->getField($fieldDefinition->identifier, $languageCode)->id,
1401
                        'fieldDefinitionId' => $fieldDefinition->id,
1402
                        'type' => $fieldDefinition->fieldTypeIdentifier,
1403
                        'value' => $fieldType->toPersistenceValue($fieldValue),
1404
                        'languageCode' => $languageCode,
1405
                        'versionNo' => $versionInfo->versionNo,
1406
                    ]
1407
                );
1408
            }
1409
        }
1410
1411
        if (!empty($allFieldErrors)) {
1412
            throw new ContentFieldValidationException($allFieldErrors);
1413
        }
1414
1415
        $spiContentUpdateStruct = new SPIContentUpdateStruct(
1416
            [
1417
                'name' => $this->nameSchemaService->resolveNameSchema(
1418
                    $content,
1419
                    $fieldValues,
1420
                    $allLanguageCodes,
1421
                    $contentType
1422
                ),
1423
                '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...
1424
                'fields' => $spiFields,
1425
                'modificationDate' => time(),
1426
                'initialLanguageId' => $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode(
1427
                    $contentUpdateStruct->initialLanguageCode
1428
                )->id,
1429
            ]
1430
        );
1431
        $existingRelations = $this->loadRelations($versionInfo);
1432
1433
        $this->repository->beginTransaction();
1434
        try {
1435
            $spiContent = $this->persistenceHandler->contentHandler()->updateContent(
1436
                $versionInfo->getContentInfo()->id,
1437
                $versionInfo->versionNo,
1438
                $spiContentUpdateStruct
1439
            );
1440
            $this->relationProcessor->processFieldRelations(
1441
                $inputRelations,
1442
                $spiContent->versionInfo->contentInfo->id,
1443
                $spiContent->versionInfo->versionNo,
1444
                $contentType,
1445
                $existingRelations
1446
            );
1447
            $this->repository->commit();
1448
        } catch (Exception $e) {
1449
            $this->repository->rollback();
1450
            throw $e;
1451
        }
1452
1453
        return $this->domainMapper->buildContentDomainObject(
1454
            $spiContent,
1455
            $contentType
1456
        );
1457
    }
1458
1459
    /**
1460
     * Returns only updated language codes.
1461
     *
1462
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1463
     *
1464
     * @return array
1465
     */
1466
    private function getUpdatedLanguageCodes(APIContentUpdateStruct $contentUpdateStruct)
1467
    {
1468
        $languageCodes = [
1469
            $contentUpdateStruct->initialLanguageCode => true,
1470
        ];
1471
1472
        foreach ($contentUpdateStruct->fields as $field) {
1473
            if ($field->languageCode === null || isset($languageCodes[$field->languageCode])) {
1474
                continue;
1475
            }
1476
1477
            $languageCodes[$field->languageCode] = true;
1478
        }
1479
1480
        return array_keys($languageCodes);
1481
    }
1482
1483
    /**
1484
     * Returns all language codes used in given $fields.
1485
     *
1486
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if no field value exists in initial language
1487
     *
1488
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1489
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1490
     *
1491
     * @return array
1492
     */
1493
    protected function getLanguageCodesForUpdate(APIContentUpdateStruct $contentUpdateStruct, APIContent $content)
1494
    {
1495
        $languageCodes = array_fill_keys($content->versionInfo->languageCodes, true);
1496
        $languageCodes[$contentUpdateStruct->initialLanguageCode] = true;
1497
1498
        $updatedLanguageCodes = $this->getUpdatedLanguageCodes($contentUpdateStruct);
1499
        foreach ($updatedLanguageCodes as $languageCode) {
1500
            $languageCodes[$languageCode] = true;
1501
        }
1502
1503
        return array_keys($languageCodes);
1504
    }
1505
1506
    /**
1507
     * Returns an array of fields like $fields[$field->fieldDefIdentifier][$field->languageCode].
1508
     *
1509
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType
1510
     *                                                                          or value is set for non-translatable field in language
1511
     *                                                                          other than main
1512
     *
1513
     * @param \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct $contentUpdateStruct
1514
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
1515
     * @param string $mainLanguageCode
1516
     *
1517
     * @return array
1518
     */
1519
    protected function mapFieldsForUpdate(
1520
        APIContentUpdateStruct $contentUpdateStruct,
1521
        ContentType $contentType,
1522
        $mainLanguageCode
1523
    ) {
1524
        $fields = [];
1525
1526
        foreach ($contentUpdateStruct->fields as $field) {
1527
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1528
1529
            if ($fieldDefinition === null) {
1530
                throw new ContentValidationException(
1531
                    "Field definition '%identifier%' does not exist in given ContentType",
1532
                    ['%identifier%' => $field->fieldDefIdentifier]
1533
                );
1534
            }
1535
1536
            if ($field->languageCode === null) {
1537
                if ($fieldDefinition->isTranslatable) {
1538
                    $languageCode = $contentUpdateStruct->initialLanguageCode;
1539
                } else {
1540
                    $languageCode = $mainLanguageCode;
1541
                }
1542
                $field = $this->cloneField($field, ['languageCode' => $languageCode]);
1543
            }
1544
1545
            if (!$fieldDefinition->isTranslatable && ($field->languageCode != $mainLanguageCode)) {
1546
                throw new ContentValidationException(
1547
                    "A value is set for non translatable field definition '%identifier%' with language '%languageCode%'",
1548
                    ['%identifier%' => $field->fieldDefIdentifier, '%languageCode%' => $field->languageCode]
1549
                );
1550
            }
1551
1552
            $fields[$field->fieldDefIdentifier][$field->languageCode] = $field;
1553
        }
1554
1555
        return $fields;
1556
    }
1557
1558
    /**
1559
     * Publishes a content version.
1560
     *
1561
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1562
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1563
     *
1564
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1565
     * @param string[] $translations
1566
     *
1567
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1568
     *
1569
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1570
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1571
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1572
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1573
     */
1574
    public function publishVersion(APIVersionInfo $versionInfo, array $translations = Language::ALL)
1575
    {
1576
        $content = $this->internalLoadContent(
1577
            $versionInfo->contentInfo->id,
1578
            null,
1579
            $versionInfo->versionNo
1580
        );
1581
1582
        $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...
1583
        if ($content->contentInfo->currentVersionNo !== $versionInfo->versionNo) {
1584
            $fromContent = $this->internalLoadContent(
1585
                $content->contentInfo->id,
1586
                null,
1587
                $content->contentInfo->currentVersionNo
1588
            );
1589
            // should not occur now, might occur in case of un-publish
1590
            if (!$fromContent->contentInfo->isPublished()) {
1591
                $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...
1592
            }
1593
        }
1594
1595
        if (!$this->repository->getPermissionResolver()->canUser(
1596
            'content',
1597
            'publish',
1598
            $content
1599
        )) {
1600
            throw new UnauthorizedException(
1601
                'content', 'publish', ['contentId' => $content->id]
1602
            );
1603
        }
1604
1605
        $this->repository->beginTransaction();
1606
        try {
1607
            $this->copyTranslationsFromPublishedVersion($content->versionInfo, $translations);
1608
            $content = $this->internalPublishVersion($content->getVersionInfo(), null);
1609
            $this->repository->commit();
1610
        } catch (Exception $e) {
1611
            $this->repository->rollback();
1612
            throw $e;
1613
        }
1614
1615
        return $content;
1616
    }
1617
1618
    /**
1619
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1620
     * @param array $translations
1621
     *
1622
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
1623
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException
1624
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
1625
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
1626
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
1627
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
1628
     */
1629
    protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionInfo, array $translations = []): void
1630
    {
1631
        $contendId = $versionInfo->contentInfo->id;
1632
1633
        $currentContent = $this->internalLoadContent($contendId);
1634
        $currentVersionInfo = $currentContent->versionInfo;
1635
1636
        // Copying occurs only if:
1637
        // - There is published Version
1638
        // - Published version is older than the currently published one unless specific translations are provided.
1639
        if (!$currentVersionInfo->isPublished() ||
1640
            ($versionInfo->versionNo >= $currentVersionInfo->versionNo && empty($translations))) {
1641
            return;
1642
        }
1643
1644
        if (empty($translations)) {
1645
            $languagesToCopy = array_diff(
1646
                $currentVersionInfo->languageCodes,
1647
                $versionInfo->languageCodes
1648
            );
1649
        } else {
1650
            $languagesToCopy = array_diff(
1651
                $currentVersionInfo->languageCodes,
1652
                $translations
1653
            );
1654
        }
1655
1656
        if (empty($languagesToCopy)) {
1657
            return;
1658
        }
1659
1660
        $contentType = $this->repository->getContentTypeService()->loadContentType(
1661
            $currentVersionInfo->contentInfo->contentTypeId
1662
        );
1663
1664
        // Find only translatable fields to update with selected languages
1665
        $updateStruct = $this->newContentUpdateStruct();
1666
        $updateStruct->initialLanguageCode = $versionInfo->initialLanguageCode;
1667
1668
        foreach ($currentContent->getFields() as $field) {
1669
            $fieldDefinition = $contentType->getFieldDefinition($field->fieldDefIdentifier);
1670
1671
            if ($fieldDefinition->isTranslatable && in_array($field->languageCode, $languagesToCopy)) {
1672
                $updateStruct->setField($field->fieldDefIdentifier, $field->value, $field->languageCode);
1673
            }
1674
        }
1675
1676
        $this->updateContent($versionInfo, $updateStruct);
1677
    }
1678
1679
    /**
1680
     * Publishes a content version.
1681
     *
1682
     * Publishes a content version and deletes archive versions if they overflow max archive versions.
1683
     * Max archive versions are currently a configuration, but might be moved to be a param of ContentType in the future.
1684
     *
1685
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
1686
     *
1687
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1688
     * @param int|null $publicationDate If null existing date is kept if there is one, otherwise current time is used.
1689
     *
1690
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1691
     */
1692
    protected function internalPublishVersion(APIVersionInfo $versionInfo, $publicationDate = null)
1693
    {
1694
        if (!$versionInfo->isDraft()) {
1695
            throw new BadStateException('$versionInfo', 'Only versions in draft status can be published.');
1696
        }
1697
1698
        $currentTime = $this->getUnixTimestamp();
1699
        if ($publicationDate === null && $versionInfo->versionNo === 1) {
1700
            $publicationDate = $currentTime;
1701
        }
1702
1703
        $metadataUpdateStruct = new SPIMetadataUpdateStruct();
1704
        $metadataUpdateStruct->publicationDate = $publicationDate;
1705
        $metadataUpdateStruct->modificationDate = $currentTime;
1706
1707
        $contentId = $versionInfo->getContentInfo()->id;
1708
        $spiContent = $this->persistenceHandler->contentHandler()->publish(
1709
            $contentId,
1710
            $versionInfo->versionNo,
1711
            $metadataUpdateStruct
1712
        );
1713
1714
        $content = $this->domainMapper->buildContentDomainObject(
1715
            $spiContent,
1716
            $this->repository->getContentTypeService()->loadContentType(
1717
                $spiContent->versionInfo->contentInfo->contentTypeId
1718
            )
1719
        );
1720
1721
        $this->publishUrlAliasesForContent($content);
1722
1723
        // Delete version archive overflow if any, limit is 0-50 (however 0 will mean 1 if content is unpublished)
1724
        $archiveList = $this->persistenceHandler->contentHandler()->listVersions(
1725
            $contentId,
1726
            APIVersionInfo::STATUS_ARCHIVED,
1727
            100 // Limited to avoid publishing taking to long, besides SE limitations this is why limit is max 50
1728
        );
1729
1730
        $maxVersionArchiveCount = max(0, min(50, $this->settings['default_version_archive_limit']));
1731
        while (!empty($archiveList) && count($archiveList) > $maxVersionArchiveCount) {
1732
            /** @var \eZ\Publish\SPI\Persistence\Content\VersionInfo $archiveVersion */
1733
            $archiveVersion = array_shift($archiveList);
1734
            $this->persistenceHandler->contentHandler()->deleteVersion(
1735
                $contentId,
1736
                $archiveVersion->versionNo
1737
            );
1738
        }
1739
1740
        return $content;
1741
    }
1742
1743
    /**
1744
     * @return int
1745
     */
1746
    protected function getUnixTimestamp()
1747
    {
1748
        return time();
1749
    }
1750
1751
    /**
1752
     * Removes the given version.
1753
     *
1754
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is in
1755
     *         published state or is a last version of Content in non draft state
1756
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to remove this version
1757
     *
1758
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1759
     */
1760
    public function deleteVersion(APIVersionInfo $versionInfo)
1761
    {
1762
        if ($versionInfo->isPublished()) {
1763
            throw new BadStateException(
1764
                '$versionInfo',
1765
                'Version is published and can not be removed'
1766
            );
1767
        }
1768
1769
        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...
1770
            throw new UnauthorizedException(
1771
                'content',
1772
                'versionremove',
1773
                ['contentId' => $versionInfo->contentInfo->id, 'versionNo' => $versionInfo->versionNo]
1774
            );
1775
        }
1776
1777
        $versionList = $this->persistenceHandler->contentHandler()->listVersions(
1778
            $versionInfo->contentInfo->id,
1779
            null,
1780
            2
1781
        );
1782
1783
        if (count($versionList) === 1 && !$versionInfo->isDraft()) {
1784
            throw new BadStateException(
1785
                '$versionInfo',
1786
                'Version is the last version of the Content and can not be removed'
1787
            );
1788
        }
1789
1790
        $this->repository->beginTransaction();
1791
        try {
1792
            $this->persistenceHandler->contentHandler()->deleteVersion(
1793
                $versionInfo->getContentInfo()->id,
1794
                $versionInfo->versionNo
1795
            );
1796
            $this->repository->commit();
1797
        } catch (Exception $e) {
1798
            $this->repository->rollback();
1799
            throw $e;
1800
        }
1801
    }
1802
1803
    /**
1804
     * Loads all versions for the given content.
1805
     *
1806
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to list versions
1807
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the given status is invalid
1808
     *
1809
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1810
     * @param int|null $status
1811
     *
1812
     * @return \eZ\Publish\API\Repository\Values\Content\VersionInfo[] Sorted by creation date
1813
     */
1814
    public function loadVersions(ContentInfo $contentInfo, ?int $status = null)
1815
    {
1816
        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...
1817
            throw new UnauthorizedException('content', 'versionread', ['contentId' => $contentInfo->id]);
1818
        }
1819
1820
        if ($status !== null && !in_array((int)$status, [VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED], true)) {
1821
            throw new InvalidArgumentException(
1822
                'status',
1823
                sprintf(
1824
                    'it can be one of %d (draft), %d (published), %d (archived), %d given',
1825
                    VersionInfo::STATUS_DRAFT, VersionInfo::STATUS_PUBLISHED, VersionInfo::STATUS_ARCHIVED, $status
1826
                ));
1827
        }
1828
1829
        $spiVersionInfoList = $this->persistenceHandler->contentHandler()->listVersions($contentInfo->id, $status);
1830
1831
        $versions = [];
1832
        foreach ($spiVersionInfoList as $spiVersionInfo) {
1833
            $versionInfo = $this->domainMapper->buildVersionInfoDomainObject($spiVersionInfo);
1834
            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...
1835
                throw new UnauthorizedException('content', 'versionread', ['versionId' => $versionInfo->id]);
1836
            }
1837
1838
            $versions[] = $versionInfo;
1839
        }
1840
1841
        return $versions;
1842
    }
1843
1844
    /**
1845
     * Copies the content to a new location. If no version is given,
1846
     * all versions are copied, otherwise only the given version.
1847
     *
1848
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to copy the content to the given location
1849
     *
1850
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1851
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $destinationLocationCreateStruct the target location where the content is copied to
1852
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1853
     *
1854
     * @return \eZ\Publish\API\Repository\Values\Content\Content
1855
     */
1856
    public function copyContent(ContentInfo $contentInfo, LocationCreateStruct $destinationLocationCreateStruct, APIVersionInfo $versionInfo = null)
1857
    {
1858
        $destinationLocation = $this->repository->getLocationService()->loadLocation(
1859
            $destinationLocationCreateStruct->parentLocationId
1860
        );
1861
        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...
1862
            throw new UnauthorizedException(
1863
                'content',
1864
                'create',
1865
                [
1866
                    'parentLocationId' => $destinationLocationCreateStruct->parentLocationId,
1867
                    'sectionId' => $contentInfo->sectionId,
1868
                ]
1869
            );
1870
        }
1871
        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...
1872
            throw new UnauthorizedException('content', 'manage_locations', ['contentId' => $contentInfo->id]);
1873
        }
1874
1875
        $defaultObjectStates = $this->getDefaultObjectStates();
1876
1877
        $this->repository->beginTransaction();
1878
        try {
1879
            $spiContent = $this->persistenceHandler->contentHandler()->copy(
1880
                $contentInfo->id,
1881
                $versionInfo ? $versionInfo->versionNo : null,
1882
                $this->repository->getPermissionResolver()->getCurrentUserReference()->getUserId()
1883
            );
1884
1885
            $objectStateHandler = $this->persistenceHandler->objectStateHandler();
1886
            foreach ($defaultObjectStates as $objectStateGroupId => $objectState) {
1887
                $objectStateHandler->setContentState(
1888
                    $spiContent->versionInfo->contentInfo->id,
1889
                    $objectStateGroupId,
1890
                    $objectState->id
1891
                );
1892
            }
1893
1894
            $content = $this->internalPublishVersion(
1895
                $this->domainMapper->buildVersionInfoDomainObject($spiContent->versionInfo),
1896
                $spiContent->versionInfo->creationDate
1897
            );
1898
1899
            $this->repository->getLocationService()->createLocation(
1900
                $content->getVersionInfo()->getContentInfo(),
1901
                $destinationLocationCreateStruct
1902
            );
1903
            $this->repository->commit();
1904
        } catch (Exception $e) {
1905
            $this->repository->rollback();
1906
            throw $e;
1907
        }
1908
1909
        return $this->internalLoadContent($content->id);
1910
    }
1911
1912
    /**
1913
     * Loads all outgoing relations for the given version.
1914
     *
1915
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1916
     *
1917
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo
1918
     *
1919
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1920
     */
1921
    public function loadRelations(APIVersionInfo $versionInfo)
1922
    {
1923
        if ($versionInfo->isPublished()) {
1924
            $function = 'read';
1925
        } else {
1926
            $function = 'versionread';
1927
        }
1928
1929
        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...
1930
            throw new UnauthorizedException('content', $function);
1931
        }
1932
1933
        $contentInfo = $versionInfo->getContentInfo();
1934
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
1935
            $contentInfo->id,
1936
            $versionInfo->versionNo
1937
        );
1938
1939
        /** @var $relations \eZ\Publish\API\Repository\Values\Content\Relation[] */
1940
        $relations = [];
1941
        foreach ($spiRelations as $spiRelation) {
1942
            $destinationContentInfo = $this->internalLoadContentInfo($spiRelation->destinationContentId);
1943
            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...
1944
                continue;
1945
            }
1946
1947
            $relations[] = $this->domainMapper->buildRelationDomainObject(
1948
                $spiRelation,
1949
                $contentInfo,
1950
                $destinationContentInfo
1951
            );
1952
        }
1953
1954
        return $relations;
1955
    }
1956
1957
    /**
1958
     * Loads all incoming relations for a content object.
1959
     *
1960
     * The relations come only from published versions of the source content objects
1961
     *
1962
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read this version
1963
     *
1964
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
1965
     *
1966
     * @return \eZ\Publish\API\Repository\Values\Content\Relation[]
1967
     */
1968
    public function loadReverseRelations(ContentInfo $contentInfo)
1969
    {
1970
        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...
1971
            throw new UnauthorizedException('content', 'reverserelatedlist', ['contentId' => $contentInfo->id]);
1972
        }
1973
1974
        $spiRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations(
1975
            $contentInfo->id
1976
        );
1977
1978
        $returnArray = [];
1979
        foreach ($spiRelations as $spiRelation) {
1980
            $sourceContentInfo = $this->internalLoadContentInfo($spiRelation->sourceContentId);
1981
            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...
1982
                continue;
1983
            }
1984
1985
            $returnArray[] = $this->domainMapper->buildRelationDomainObject(
1986
                $spiRelation,
1987
                $sourceContentInfo,
1988
                $contentInfo
1989
            );
1990
        }
1991
1992
        return $returnArray;
1993
    }
1994
1995
    /**
1996
     * Adds a relation of type common.
1997
     *
1998
     * The source of the relation is the content and version
1999
     * referenced by $versionInfo.
2000
     *
2001
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to edit this version
2002
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2003
     *
2004
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2005
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent the destination of the relation
2006
     *
2007
     * @return \eZ\Publish\API\Repository\Values\Content\Relation the newly created relation
2008
     */
2009
    public function addRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
2010
    {
2011
        $sourceVersion = $this->loadVersionInfoById(
2012
            $sourceVersion->contentInfo->id,
2013
            $sourceVersion->versionNo
2014
        );
2015
2016
        if (!$sourceVersion->isDraft()) {
2017
            throw new BadStateException(
2018
                '$sourceVersion',
2019
                'Relations of type common can only be added to versions of status draft'
2020
            );
2021
        }
2022
2023
        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...
2024
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2025
        }
2026
2027
        $sourceContentInfo = $sourceVersion->getContentInfo();
2028
2029
        $this->repository->beginTransaction();
2030
        try {
2031
            $spiRelation = $this->persistenceHandler->contentHandler()->addRelation(
2032
                new SPIRelationCreateStruct(
2033
                    [
2034
                        'sourceContentId' => $sourceContentInfo->id,
2035
                        'sourceContentVersionNo' => $sourceVersion->versionNo,
2036
                        'sourceFieldDefinitionId' => null,
2037
                        'destinationContentId' => $destinationContent->id,
2038
                        'type' => APIRelation::COMMON,
2039
                    ]
2040
                )
2041
            );
2042
            $this->repository->commit();
2043
        } catch (Exception $e) {
2044
            $this->repository->rollback();
2045
            throw $e;
2046
        }
2047
2048
        return $this->domainMapper->buildRelationDomainObject($spiRelation, $sourceContentInfo, $destinationContent);
2049
    }
2050
2051
    /**
2052
     * Removes a relation of type COMMON from a draft.
2053
     *
2054
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed edit this version
2055
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
2056
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is no relation of type COMMON for the given destination
2057
     *
2058
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $sourceVersion
2059
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $destinationContent
2060
     */
2061
    public function deleteRelation(APIVersionInfo $sourceVersion, ContentInfo $destinationContent)
2062
    {
2063
        $sourceVersion = $this->loadVersionInfoById(
2064
            $sourceVersion->contentInfo->id,
2065
            $sourceVersion->versionNo
2066
        );
2067
2068
        if (!$sourceVersion->isDraft()) {
2069
            throw new BadStateException(
2070
                '$sourceVersion',
2071
                'Relations of type common can only be removed from versions of status draft'
2072
            );
2073
        }
2074
2075
        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...
2076
            throw new UnauthorizedException('content', 'edit', ['contentId' => $sourceVersion->contentInfo->id]);
2077
        }
2078
2079
        $spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
2080
            $sourceVersion->getContentInfo()->id,
2081
            $sourceVersion->versionNo,
2082
            APIRelation::COMMON
2083
        );
2084
2085
        if (empty($spiRelations)) {
2086
            throw new InvalidArgumentException(
2087
                '$sourceVersion',
2088
                'There are no relations of type COMMON for the given destination'
2089
            );
2090
        }
2091
2092
        // there should be only one relation of type COMMON for each destination,
2093
        // but in case there were ever more then one, we will remove them all
2094
        // @todo: alternatively, throw BadStateException?
2095
        $this->repository->beginTransaction();
2096
        try {
2097
            foreach ($spiRelations as $spiRelation) {
2098
                if ($spiRelation->destinationContentId == $destinationContent->id) {
2099
                    $this->persistenceHandler->contentHandler()->removeRelation(
2100
                        $spiRelation->id,
2101
                        APIRelation::COMMON
2102
                    );
2103
                }
2104
            }
2105
            $this->repository->commit();
2106
        } catch (Exception $e) {
2107
            $this->repository->rollback();
2108
            throw $e;
2109
        }
2110
    }
2111
2112
    /**
2113
     * {@inheritdoc}
2114
     */
2115
    public function removeTranslation(ContentInfo $contentInfo, $languageCode)
2116
    {
2117
        @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...
2118
            __METHOD__ . ' is deprecated, use deleteTranslation instead',
2119
            E_USER_DEPRECATED
2120
        );
2121
        $this->deleteTranslation($contentInfo, $languageCode);
2122
    }
2123
2124
    /**
2125
     * Delete Content item Translation from all Versions (including archived ones) of a Content Object.
2126
     *
2127
     * NOTE: this operation is risky and permanent, so user interface should provide a warning before performing it.
2128
     *
2129
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2130
     *         is the Main Translation of a Content Item.
2131
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2132
     *         to delete the content (in one of the locations of the given Content Item).
2133
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2134
     *         is invalid for the given content.
2135
     *
2136
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2137
     * @param string $languageCode
2138
     *
2139
     * @since 6.13
2140
     */
2141
    public function deleteTranslation(ContentInfo $contentInfo, $languageCode)
2142
    {
2143
        if ($contentInfo->mainLanguageCode === $languageCode) {
2144
            throw new BadStateException(
2145
                '$languageCode',
2146
                'Specified translation is the main translation of the Content Object'
2147
            );
2148
        }
2149
2150
        $translationWasFound = false;
2151
        $this->repository->beginTransaction();
2152
        try {
2153
            foreach ($this->loadVersions($contentInfo) as $versionInfo) {
2154
                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...
2155
                    throw new UnauthorizedException(
2156
                        'content',
2157
                        'remove',
2158
                        ['contentId' => $contentInfo->id, 'versionNo' => $versionInfo->versionNo]
2159
                    );
2160
                }
2161
2162
                if (!in_array($languageCode, $versionInfo->languageCodes)) {
2163
                    continue;
2164
                }
2165
2166
                $translationWasFound = true;
2167
2168
                // If the translation is the version's only one, delete the version
2169
                if (count($versionInfo->languageCodes) < 2) {
2170
                    $this->persistenceHandler->contentHandler()->deleteVersion(
2171
                        $versionInfo->getContentInfo()->id,
2172
                        $versionInfo->versionNo
2173
                    );
2174
                }
2175
            }
2176
2177
            if (!$translationWasFound) {
2178
                throw new InvalidArgumentException(
2179
                    '$languageCode',
2180
                    sprintf(
2181
                        '%s does not exist in the Content item(id=%d)',
2182
                        $languageCode,
2183
                        $contentInfo->id
2184
                    )
2185
                );
2186
            }
2187
2188
            $this->persistenceHandler->contentHandler()->deleteTranslationFromContent(
2189
                $contentInfo->id,
2190
                $languageCode
2191
            );
2192
            $locationIds = array_map(
2193
                function (Location $location) {
2194
                    return $location->id;
2195
                },
2196
                $this->repository->getLocationService()->loadLocations($contentInfo)
2197
            );
2198
            $this->persistenceHandler->urlAliasHandler()->translationRemoved(
2199
                $locationIds,
2200
                $languageCode
2201
            );
2202
            $this->repository->commit();
2203
        } catch (InvalidArgumentException $e) {
2204
            $this->repository->rollback();
2205
            throw $e;
2206
        } catch (BadStateException $e) {
2207
            $this->repository->rollback();
2208
            throw $e;
2209
        } catch (UnauthorizedException $e) {
2210
            $this->repository->rollback();
2211
            throw $e;
2212
        } catch (Exception $e) {
2213
            $this->repository->rollback();
2214
            // cover generic unexpected exception to fulfill API promise on @throws
2215
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2216
        }
2217
    }
2218
2219
    /**
2220
     * Delete specified Translation from a Content Draft.
2221
     *
2222
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the specified Translation
2223
     *         is the only one the Content Draft has or it is the main Translation of a Content Object.
2224
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed
2225
     *         to edit the Content (in one of the locations of the given Content Object).
2226
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if languageCode argument
2227
     *         is invalid for the given Draft.
2228
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if specified Version was not found
2229
     *
2230
     * @param \eZ\Publish\API\Repository\Values\Content\VersionInfo $versionInfo Content Version Draft
2231
     * @param string $languageCode Language code of the Translation to be removed
2232
     *
2233
     * @return \eZ\Publish\API\Repository\Values\Content\Content Content Draft w/o the specified Translation
2234
     *
2235
     * @since 6.12
2236
     */
2237
    public function deleteTranslationFromDraft(APIVersionInfo $versionInfo, $languageCode)
2238
    {
2239
        if (!$versionInfo->isDraft()) {
2240
            throw new BadStateException(
2241
                '$versionInfo',
2242
                'Version is not a draft, so Translations cannot be modified. Create a Draft before proceeding'
2243
            );
2244
        }
2245
2246
        if ($versionInfo->contentInfo->mainLanguageCode === $languageCode) {
2247
            throw new BadStateException(
2248
                '$languageCode',
2249
                'Specified Translation is the main Translation of the Content Object. Change it before proceeding.'
2250
            );
2251
        }
2252
2253
        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...
2254
            throw new UnauthorizedException(
2255
                'content', 'edit', ['contentId' => $versionInfo->contentInfo->id]
2256
            );
2257
        }
2258
2259
        if (!in_array($languageCode, $versionInfo->languageCodes)) {
2260
            throw new InvalidArgumentException(
2261
                '$languageCode',
2262
                sprintf(
2263
                    'The Version (ContentId=%d, VersionNo=%d) is not translated into %s',
2264
                    $versionInfo->contentInfo->id,
2265
                    $versionInfo->versionNo,
2266
                    $languageCode
2267
                )
2268
            );
2269
        }
2270
2271
        if (count($versionInfo->languageCodes) === 1) {
2272
            throw new BadStateException(
2273
                '$languageCode',
2274
                'Specified Translation is the only one Content Object Version has'
2275
            );
2276
        }
2277
2278
        $this->repository->beginTransaction();
2279
        try {
2280
            $spiContent = $this->persistenceHandler->contentHandler()->deleteTranslationFromDraft(
2281
                $versionInfo->contentInfo->id,
2282
                $versionInfo->versionNo,
2283
                $languageCode
2284
            );
2285
            $this->repository->commit();
2286
2287
            return $this->domainMapper->buildContentDomainObject(
2288
                $spiContent,
2289
                $this->repository->getContentTypeService()->loadContentType(
2290
                    $spiContent->versionInfo->contentInfo->contentTypeId
2291
                )
2292
            );
2293
        } catch (APINotFoundException $e) {
2294
            // avoid wrapping expected NotFoundException in BadStateException handled below
2295
            $this->repository->rollback();
2296
            throw $e;
2297
        } catch (Exception $e) {
2298
            $this->repository->rollback();
2299
            // cover generic unexpected exception to fulfill API promise on @throws
2300
            throw new BadStateException('$contentInfo', 'Translation removal failed', $e);
2301
        }
2302
    }
2303
2304
    /**
2305
     * Hides Content by making all the Locations appear hidden.
2306
     * It does not persist hidden state on Location object itself.
2307
     *
2308
     * Content hidden by this API can be revealed by revealContent API.
2309
     *
2310
     * @see revealContent
2311
     *
2312
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2313
     */
2314
    public function hideContent(ContentInfo $contentInfo): void
2315
    {
2316
        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...
2317
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2318
        }
2319
2320
        $this->repository->beginTransaction();
2321
        try {
2322
            $this->persistenceHandler->contentHandler()->updateMetadata(
2323
                $contentInfo->id,
2324
                new SPIMetadataUpdateStruct([
2325
                    'isHidden' => true,
2326
                ])
2327
            );
2328
            $locationHandler = $this->persistenceHandler->locationHandler();
2329
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2330
            foreach ($childLocations as $childLocation) {
2331
                $locationHandler->setInvisible($childLocation->id);
2332
            }
2333
            $this->repository->commit();
2334
        } catch (Exception $e) {
2335
            $this->repository->rollback();
2336
            throw $e;
2337
        }
2338
    }
2339
2340
    /**
2341
     * Reveals Content hidden by hideContent API.
2342
     * Locations which were hidden before hiding Content will remain hidden.
2343
     *
2344
     * @see hideContent
2345
     *
2346
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
2347
     */
2348
    public function revealContent(ContentInfo $contentInfo): void
2349
    {
2350
        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...
2351
            throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]);
2352
        }
2353
2354
        $this->repository->beginTransaction();
2355
        try {
2356
            $this->persistenceHandler->contentHandler()->updateMetadata(
2357
                $contentInfo->id,
2358
                new SPIMetadataUpdateStruct([
2359
                    'isHidden' => false,
2360
                ])
2361
            );
2362
            $locationHandler = $this->persistenceHandler->locationHandler();
2363
            $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id);
2364
            foreach ($childLocations as $childLocation) {
2365
                $locationHandler->setVisible($childLocation->id);
2366
            }
2367
            $this->repository->commit();
2368
        } catch (Exception $e) {
2369
            $this->repository->rollback();
2370
            throw $e;
2371
        }
2372
    }
2373
2374
    /**
2375
     * Instantiates a new content create struct object.
2376
     *
2377
     * alwaysAvailable is set to the ContentType's defaultAlwaysAvailable
2378
     *
2379
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
2380
     * @param string $mainLanguageCode
2381
     *
2382
     * @return \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct
2383
     */
2384
    public function newContentCreateStruct(ContentType $contentType, $mainLanguageCode)
2385
    {
2386
        return new ContentCreateStruct(
2387
            [
2388
                'contentType' => $contentType,
2389
                'mainLanguageCode' => $mainLanguageCode,
2390
                'alwaysAvailable' => $contentType->defaultAlwaysAvailable,
2391
            ]
2392
        );
2393
    }
2394
2395
    /**
2396
     * Instantiates a new content meta data update struct.
2397
     *
2398
     * @return \eZ\Publish\API\Repository\Values\Content\ContentMetadataUpdateStruct
2399
     */
2400
    public function newContentMetadataUpdateStruct()
2401
    {
2402
        return new ContentMetadataUpdateStruct();
2403
    }
2404
2405
    /**
2406
     * Instantiates a new content update struct.
2407
     *
2408
     * @return \eZ\Publish\API\Repository\Values\Content\ContentUpdateStruct
2409
     */
2410
    public function newContentUpdateStruct()
2411
    {
2412
        return new ContentUpdateStruct();
2413
    }
2414
2415
    /**
2416
     * @param \eZ\Publish\API\Repository\Values\User\User|null $user
2417
     *
2418
     * @return \eZ\Publish\API\Repository\Values\User\UserReference
2419
     */
2420
    private function resolveUser(?User $user): UserReference
2421
    {
2422
        if ($user === null) {
2423
            $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...
2424
        }
2425
2426
        return $user;
2427
    }
2428
}
2429