Completed
Push — master ( 7409fa...631718 )
by
unknown
116:54 queued 93:07
created

ContentService::loadVersions()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

Loading history...
1923
            throw new UnauthorizedException('content', 'reverserelatedlist', array('contentId' => $contentInfo->id));
1924
        }
1925
1926
        $spiRelations = $this->persistenceHandler->contentHandler()->loadReverseRelations(
1927
            $contentInfo->id
1928
        );
1929
1930
        $returnArray = array();
1931
        foreach ($spiRelations as $spiRelation) {
1932
            $sourceContentInfo = $this->internalLoadContentInfo($spiRelation->sourceContentId);
1933
            if (!$this->repository->canUser('content', 'read', $sourceContentInfo)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

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

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

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