Completed
Push — 6.13 ( 8ade82...b4f000 )
by
unknown
19:34 queued 10s
created

ContentService::deleteRelation()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 10
nop 2
dl 0
loc 50
rs 8.1575
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\Core\Repository\Values\Content\Location;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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