Completed
Push — 7.2 ( 0b4a18...e81664 )
by
unknown
43:24 queued 17:27
created

ContentService::mapFieldsForUpdate()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 38

Duplication

Lines 6
Ratio 15.79 %

Importance

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