Completed
Push — master ( b175f4...a2dcbc )
by
unknown
37:39 queued 24:55
created

LocationService::createLocation()   C

Complexity

Conditions 9
Paths 20

Size

Total Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 20
nop 2
dl 0
loc 81
rs 6.8589
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
8
9
namespace eZ\Publish\Core\Repository;
10
11
use eZ\Publish\API\Repository\PermissionCriterionResolver;
12
use eZ\Publish\API\Repository\PermissionResolver;
13
use eZ\Publish\API\Repository\Values\Content\Language;
14
use eZ\Publish\API\Repository\Values\Content\Location;
15
use eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct;
16
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
17
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
18
use eZ\Publish\API\Repository\Values\Content\Location as APILocation;
19
use eZ\Publish\API\Repository\Values\Content\LocationList;
20
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
21
use eZ\Publish\Core\Repository\Mapper\ContentDomainMapper;
22
use eZ\Publish\SPI\Persistence\Content\Location as SPILocation;
23
use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
24
use eZ\Publish\API\Repository\LocationService as LocationServiceInterface;
25
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
26
use eZ\Publish\SPI\Persistence\Handler;
27
use eZ\Publish\API\Repository\Values\Content\Query;
28
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
29
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
30
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd as CriterionLogicalAnd;
31
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalNot as CriterionLogicalNot;
32
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Subtree as CriterionSubtree;
33
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
34
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
35
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
36
use eZ\Publish\Core\Base\Exceptions\BadStateException;
37
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
38
use Exception;
39
use Psr\Log\LoggerInterface;
40
use Psr\Log\NullLogger;
41
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
42
43
/**
44
 * Location service, used for complex subtree operations.
45
 *
46
 * @example Examples/location.php
47
 */
48
class LocationService implements LocationServiceInterface
49
{
50
    /** @var \eZ\Publish\Core\Repository\Repository */
51
    protected $repository;
52
53
    /** @var \eZ\Publish\SPI\Persistence\Handler */
54
    protected $persistenceHandler;
55
56
    /** @var array */
57
    protected $settings;
58
59
    /** @var \eZ\Publish\Core\Repository\Mapper\ContentDomainMapper */
60
    protected $contentDomainMapper;
61
62
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
63
    protected $nameSchemaService;
64
65
    /** @var \eZ\Publish\API\Repository\PermissionCriterionResolver */
66
    protected $permissionCriterionResolver;
67
68
    /** @var \Psr\Log\LoggerInterface */
69
    private $logger;
70
71
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
72
    private $permissionResolver;
73
74
    /**
75
     * Setups service with reference to repository object that created it & corresponding handler.
76
     *
77
     * @param \eZ\Publish\API\Repository\Repository $repository
78
     * @param \eZ\Publish\SPI\Persistence\Handler $handler
79
     * @param \eZ\Publish\Core\Repository\Mapper\ContentDomainMapper $contentDomainMapper
80
     * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
81
     * @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver
82
     * @param array $settings
83
     * @param \Psr\Log\LoggerInterface|null $logger
84
     */
85
    public function __construct(
86
        RepositoryInterface $repository,
87
        Handler $handler,
88
        ContentDomainMapper $contentDomainMapper,
89
        Helper\NameSchemaService $nameSchemaService,
90
        PermissionCriterionResolver $permissionCriterionResolver,
91
        PermissionResolver $permissionResolver,
92
        array $settings = [],
93
        LoggerInterface $logger = null
94
    ) {
95
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repository of type object<eZ\Publish\API\Repository\Repository> is incompatible with the declared type object<eZ\Publish\Core\Repository\Repository> of property $repository.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
96
        $this->persistenceHandler = $handler;
97
        $this->contentDomainMapper = $contentDomainMapper;
98
        $this->nameSchemaService = $nameSchemaService;
99
        $this->permissionResolver = $permissionResolver;
100
        // Union makes sure default settings are ignored if provided in argument
101
        $this->settings = $settings + [
102
            //'defaultSetting' => array(),
103
        ];
104
        $this->permissionCriterionResolver = $permissionCriterionResolver;
105
        $this->logger = null !== $logger ? $logger : new NullLogger();
106
    }
107
108
    /**
109
     * Copies the subtree starting from $subtree as a new subtree of $targetLocation.
110
     *
111
     * Only the items on which the user has read access are copied.
112
     *
113
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed copy the subtree to the given parent location
114
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user does not have read access to the whole source subtree
115
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the target location is a sub location of the given location
116
     *
117
     * @param \eZ\Publish\API\Repository\Values\Content\Location $subtree - the subtree denoted by the location to copy
118
     * @param \eZ\Publish\API\Repository\Values\Content\Location $targetParentLocation - the target parent location for the copy operation
119
     *
120
     * @return \eZ\Publish\API\Repository\Values\Content\Location The newly created location of the copied subtree
121
     */
122
    public function copySubtree(APILocation $subtree, APILocation $targetParentLocation): APILocation
123
    {
124
        $loadedSubtree = $this->loadLocation($subtree->id);
125
        $loadedTargetLocation = $this->loadLocation($targetParentLocation->id);
126
127
        if (stripos($loadedTargetLocation->pathString, $loadedSubtree->pathString) !== false) {
128
            throw new InvalidArgumentException('targetParentLocation', 'Cannot copy subtree to its own descendant Location');
129
        }
130
131
        // check create permission on target
132
        if (!$this->permissionResolver->canUser('content', 'create', $loadedSubtree->getContentInfo(), [$loadedTargetLocation])) {
133
            throw new UnauthorizedException('content', 'create', ['locationId' => $loadedTargetLocation->id]);
134
        }
135
136
        /** Check read access to whole source subtree
137
         * @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
138
         */
139
        $contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'read');
140
        if ($contentReadCriterion === false) {
141
            throw new UnauthorizedException('content', 'read');
142
        } elseif ($contentReadCriterion !== true) {
143
            // Query if there are any content in subtree current user don't have access to
144
            $query = new Query(
145
                [
146
                    'limit' => 0,
147
                    'filter' => new CriterionLogicalAnd(
148
                        [
149
                            new CriterionSubtree($loadedSubtree->pathString),
150
                            new CriterionLogicalNot($contentReadCriterion),
0 ignored issues
show
Bug introduced by
It seems like $contentReadCriterion defined by $this->permissionCriteri...rion('content', 'read') on line 139 can also be of type boolean; however, eZ\Publish\API\Repositor...gicalNot::__construct() does only seem to accept object<eZ\Publish\API\Re...ontent\Query\Criterion>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
151
                        ]
152
                    ),
153
                ]
154
            );
155
            $result = $this->repository->getSearchService()->findContent($query, [], false);
156
            if ($result->totalCount > 0) {
157
                throw new UnauthorizedException('content', 'read');
158
            }
159
        }
160
161
        $this->repository->beginTransaction();
162
        try {
163
            $newLocation = $this->persistenceHandler->locationHandler()->copySubtree(
164
                $loadedSubtree->id,
165
                $loadedTargetLocation->id,
166
                $this->repository->getPermissionResolver()->getCurrentUserReference()->getUserId()
167
            );
168
169
            $content = $this->repository->getContentService()->loadContent($newLocation->contentId);
170
            $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
171
            foreach ($urlAliasNames as $languageCode => $name) {
172
                $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
173
                    $newLocation->id,
174
                    $loadedTargetLocation->id,
175
                    $name,
176
                    $languageCode,
177
                    $content->contentInfo->alwaysAvailable
178
                );
179
            }
180
181
            $this->persistenceHandler->urlAliasHandler()->locationCopied(
182
                $loadedSubtree->id,
183
                $newLocation->id,
184
                $loadedTargetLocation->id
185
            );
186
187
            $this->repository->commit();
188
        } catch (Exception $e) {
189
            $this->repository->rollback();
190
            throw $e;
191
        }
192
193
        return $this->contentDomainMapper->buildLocationWithContent($newLocation, $content);
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function loadLocation(int $locationId, ?array $prioritizedLanguages = null, ?bool $useAlwaysAvailable = null): APILocation
200
    {
201
        $spiLocation = $this->persistenceHandler->locationHandler()->load($locationId, $prioritizedLanguages, $useAlwaysAvailable ?? true);
202
        $location = $this->contentDomainMapper->buildLocation($spiLocation, $prioritizedLanguages ?: [], $useAlwaysAvailable ?? true);
203
        if (!$this->permissionResolver->canUser('content', 'read', $location->getContentInfo(), [$location])) {
204
            throw new UnauthorizedException('content', 'read', ['locationId' => $location->id]);
205
        }
206
207
        return $location;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function loadLocationList(array $locationIds, ?array $prioritizedLanguages = null, ?bool $useAlwaysAvailable = null): iterable
214
    {
215
        $spiLocations = $this->persistenceHandler->locationHandler()->loadList(
216
            $locationIds,
217
            $prioritizedLanguages,
218
            $useAlwaysAvailable ?? true
219
        );
220
        if (empty($spiLocations)) {
221
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type declared by the interface eZ\Publish\API\Repositor...rvice::loadLocationList of type eZ\Publish\API\Repositor...API\Repository\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
222
        }
223
224
        // Get content id's
225
        $contentIds = [];
226
        foreach ($spiLocations as $spiLocation) {
227
            $contentIds[] = $spiLocation->contentId;
228
        }
229
230
        // Load content info and Get content proxy
231
        $spiContentInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
232
        $contentProxyList = $this->contentDomainMapper->buildContentProxyList(
233
            $spiContentInfoList,
234
            $prioritizedLanguages ?? [],
235
            $useAlwaysAvailable ?? true
236
        );
237
238
        // Build locations using the bulk retrieved content info and bulk lazy loaded content proxies.
239
        $locations = [];
240
        $permissionResolver = $this->repository->getPermissionResolver();
241
        foreach ($spiLocations as $spiLocation) {
242
            $location = $this->contentDomainMapper->buildLocationWithContent(
243
                $spiLocation,
244
                $contentProxyList[$spiLocation->contentId] ?? null,
245
                $spiContentInfoList[$spiLocation->contentId] ?? null
246
            );
247
248
            if ($permissionResolver->canUser('content', 'read', $location->getContentInfo(), [$location])) {
249
                $locations[$spiLocation->id] = $location;
250
            }
251
        }
252
253
        return $locations;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $locations; (array) is incompatible with the return type declared by the interface eZ\Publish\API\Repositor...rvice::loadLocationList of type eZ\Publish\API\Repositor...API\Repository\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function loadLocationByRemoteId(string $remoteId, ?array $prioritizedLanguages = null, ?bool $useAlwaysAvailable = null): APILocation
260
    {
261
        $spiLocation = $this->persistenceHandler->locationHandler()->loadByRemoteId($remoteId, $prioritizedLanguages, $useAlwaysAvailable ?? true);
262
        $location = $this->contentDomainMapper->buildLocation($spiLocation, $prioritizedLanguages ?: [], $useAlwaysAvailable ?? true);
263
        if (!$this->permissionResolver->canUser('content', 'read', $location->getContentInfo(), [$location])) {
264
            throw new UnauthorizedException('content', 'read', ['locationId' => $location->id]);
265
        }
266
267
        return $location;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273
    public function loadLocations(ContentInfo $contentInfo, ?APILocation $rootLocation = null, ?array $prioritizedLanguages = null): iterable
274
    {
275
        if (!$contentInfo->published) {
276
            throw new BadStateException('$contentInfo', 'The Content item has no published versions');
277
        }
278
279
        $spiLocations = $this->persistenceHandler->locationHandler()->loadLocationsByContent(
280
            $contentInfo->id,
281
            $rootLocation !== null ? $rootLocation->id : null
282
        );
283
284
        $locations = [];
285
        $spiInfo = $this->persistenceHandler->contentHandler()->loadContentInfo($contentInfo->id);
286
        $content = $this->contentDomainMapper->buildContentProxy($spiInfo, $prioritizedLanguages ?: []);
287
        foreach ($spiLocations as $spiLocation) {
288
            $location = $this->contentDomainMapper->buildLocationWithContent($spiLocation, $content, $spiInfo);
289
            if ($this->permissionResolver->canUser('content', 'read', $location->getContentInfo(), [$location])) {
290
                $locations[] = $location;
291
            }
292
        }
293
294
        return $locations;
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300
    public function loadLocationChildren(APILocation $location, int $offset = 0, int $limit = 25, ?array $prioritizedLanguages = null): LocationList
301
    {
302
        if (!$this->contentDomainMapper->isValidLocationSortField($location->sortField)) {
303
            throw new InvalidArgumentValue('sortField', $location->sortField, 'Location');
304
        }
305
306
        if (!$this->contentDomainMapper->isValidLocationSortOrder($location->sortOrder)) {
307
            throw new InvalidArgumentValue('sortOrder', $location->sortOrder, 'Location');
308
        }
309
310
        if (!is_int($offset)) {
311
            throw new InvalidArgumentValue('offset', $offset);
312
        }
313
314
        if (!is_int($limit)) {
315
            throw new InvalidArgumentValue('limit', $limit);
316
        }
317
318
        $childLocations = [];
319
        $searchResult = $this->searchChildrenLocations($location, $offset, $limit, $prioritizedLanguages ?: []);
320
        foreach ($searchResult->searchHits as $searchHit) {
321
            $childLocations[] = $searchHit->valueObject;
322
        }
323
324
        return new LocationList(
325
            [
326
                'locations' => $childLocations,
327
                'totalCount' => $searchResult->totalCount,
328
            ]
329
        );
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335
    public function loadParentLocationsForDraftContent(VersionInfo $versionInfo, ?array $prioritizedLanguages = null): iterable
336
    {
337
        if (!$versionInfo->isDraft()) {
338
            throw new BadStateException(
339
                '$contentInfo',
340
                sprintf(
341
                    'Content item [%d] %s is already published. Use LocationService::loadLocations instead.',
342
                    $versionInfo->contentInfo->id,
343
                    $versionInfo->contentInfo->name
344
                )
345
            );
346
        }
347
348
        $spiLocations = $this->persistenceHandler
349
            ->locationHandler()
350
            ->loadParentLocationsForDraftContent($versionInfo->contentInfo->id);
351
352
        $contentIds = [];
353
        foreach ($spiLocations as $spiLocation) {
354
            $contentIds[] = $spiLocation->contentId;
355
        }
356
357
        $locations = [];
358
        $permissionResolver = $this->repository->getPermissionResolver();
359
        $spiContentInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
360
        $contentList = $this->contentDomainMapper->buildContentProxyList($spiContentInfoList, $prioritizedLanguages ?: []);
361
        foreach ($spiLocations as $spiLocation) {
362
            $location = $this->contentDomainMapper->buildLocationWithContent(
363
                $spiLocation,
364
                $contentList[$spiLocation->contentId],
365
                $spiContentInfoList[$spiLocation->contentId]
366
            );
367
368
            if ($permissionResolver->canUser('content', 'read', $location->getContentInfo(), [$location])) {
369
                $locations[] = $location;
370
            }
371
        }
372
373
        return $locations;
374
    }
375
376
    /**
377
     * Returns the number of children which are readable by the current user of a location object.
378
     *
379
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
380
     *
381
     * @return int
382
     */
383
    public function getLocationChildCount(APILocation $location): int
384
    {
385
        $searchResult = $this->searchChildrenLocations($location, 0, 0);
386
387
        return $searchResult->totalCount;
388
    }
389
390
    /**
391
     * Searches children locations of the provided parent location id.
392
     *
393
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
394
     * @param int $offset
395
     * @param int $limit
396
     *
397
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
398
     */
399
    protected function searchChildrenLocations(APILocation $location, $offset = 0, $limit = -1, array $prioritizedLanguages = null)
400
    {
401
        $query = new LocationQuery([
402
            'filter' => new Criterion\ParentLocationId($location->id),
403
            'offset' => $offset >= 0 ? (int)$offset : 0,
404
            'limit' => $limit >= 0 ? (int)$limit : null,
405
            'sortClauses' => $location->getSortClauses(),
406
        ]);
407
408
        return $this->repository->getSearchService()->findLocations($query, ['languages' => $prioritizedLanguages]);
409
    }
410
411
    /**
412
     * Creates the new $location in the content repository for the given content.
413
     *
414
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to create this location
415
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the content is already below the specified parent
416
     *                                        or the parent is a sub location of the location of the content
417
     *                                        or if set the remoteId exists already
418
     *
419
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
420
     * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct $locationCreateStruct
421
     *
422
     * @return \eZ\Publish\API\Repository\Values\Content\Location the newly created Location
423
     */
424
    public function createLocation(ContentInfo $contentInfo, LocationCreateStruct $locationCreateStruct): APILocation
425
    {
426
        $content = $this->contentDomainMapper->buildContentDomainObjectFromPersistence(
427
            $this->persistenceHandler->contentHandler()->load($contentInfo->id),
428
            $this->persistenceHandler->contentTypeHandler()->load($contentInfo->contentTypeId)
429
        );
430
431
        $parentLocation = $this->contentDomainMapper->buildLocation(
432
            $this->persistenceHandler->locationHandler()->load($locationCreateStruct->parentLocationId)
433
        );
434
435
        $contentType = $content->getContentType();
436
437
        $locationCreateStruct->sortField = $locationCreateStruct->sortField
438
            ?? ($contentType->defaultSortField ?? Location::SORT_FIELD_NAME);
439
        $locationCreateStruct->sortOrder = $locationCreateStruct->sortOrder
440
            ?? ($contentType->defaultSortOrder ?? Location::SORT_ORDER_ASC);
441
442
        $contentInfo = $content->contentInfo;
443
444
        if (!$this->permissionResolver->canUser('content', 'manage_locations', $contentInfo, [$parentLocation])) {
445
            throw new UnauthorizedException('content', 'manage_locations', ['contentId' => $contentInfo->id]);
446
        }
447
448
        if (!$this->permissionResolver->canUser('content', 'create', $contentInfo, [$parentLocation])) {
449
            throw new UnauthorizedException('content', 'create', ['locationId' => $parentLocation->id]);
450
        }
451
452
        // Check if the parent is a sub location of one of the existing content locations (this also solves the
453
        // situation where parent location actually one of the content locations),
454
        // or if the content already has location below given location create struct parent
455
        $existingContentLocations = $this->loadLocations($contentInfo);
456
        if (!empty($existingContentLocations)) {
457
            foreach ($existingContentLocations as $existingContentLocation) {
458
                if (stripos($parentLocation->pathString, $existingContentLocation->pathString) !== false) {
459
                    throw new InvalidArgumentException(
460
                        '$locationCreateStruct',
461
                        'Specified parent is a descendant of one of the existing Locations of this content.'
462
                    );
463
                }
464
                if ($parentLocation->id == $existingContentLocation->parentLocationId) {
465
                    throw new InvalidArgumentException(
466
                        '$locationCreateStruct',
467
                        'Content is already below the specified parent.'
468
                    );
469
                }
470
            }
471
        }
472
473
        $spiLocationCreateStruct = $this->contentDomainMapper->buildSPILocationCreateStruct(
474
            $locationCreateStruct,
475
            $parentLocation,
476
            $contentInfo->mainLocationId ?? true,
477
            $contentInfo->id,
478
            $contentInfo->currentVersionNo
479
        );
480
481
        $this->repository->beginTransaction();
482
        try {
483
            $newLocation = $this->persistenceHandler->locationHandler()->create($spiLocationCreateStruct);
484
            $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
485
            foreach ($urlAliasNames as $languageCode => $name) {
486
                $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
487
                    $newLocation->id,
488
                    $newLocation->parentId,
489
                    $name,
490
                    $languageCode,
491
                    $contentInfo->alwaysAvailable,
492
                    // @todo: this is legacy storage specific for updating ezcontentobject_tree.path_identification_string, to be removed
493
                    $languageCode === $contentInfo->mainLanguageCode
494
                );
495
            }
496
497
            $this->repository->commit();
498
        } catch (Exception $e) {
499
            $this->repository->rollback();
500
            throw $e;
501
        }
502
503
        return $this->contentDomainMapper->buildLocationWithContent($newLocation, $content);
504
    }
505
506
    /**
507
     * Updates $location in the content repository.
508
     *
509
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to update this location
510
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException   if if set the remoteId exists already
511
     *
512
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
513
     * @param \eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct $locationUpdateStruct
514
     *
515
     * @return \eZ\Publish\API\Repository\Values\Content\Location the updated Location
516
     */
517
    public function updateLocation(APILocation $location, LocationUpdateStruct $locationUpdateStruct): APILocation
518
    {
519
        if (!$this->contentDomainMapper->isValidLocationPriority($locationUpdateStruct->priority)) {
520
            throw new InvalidArgumentValue('priority', $locationUpdateStruct->priority, 'LocationUpdateStruct');
521
        }
522
523
        if ($locationUpdateStruct->remoteId !== null && (!is_string($locationUpdateStruct->remoteId) || empty($locationUpdateStruct->remoteId))) {
524
            throw new InvalidArgumentValue('remoteId', $locationUpdateStruct->remoteId, 'LocationUpdateStruct');
525
        }
526
527
        if ($locationUpdateStruct->sortField !== null && !$this->contentDomainMapper->isValidLocationSortField($locationUpdateStruct->sortField)) {
528
            throw new InvalidArgumentValue('sortField', $locationUpdateStruct->sortField, 'LocationUpdateStruct');
529
        }
530
531
        if ($locationUpdateStruct->sortOrder !== null && !$this->contentDomainMapper->isValidLocationSortOrder($locationUpdateStruct->sortOrder)) {
532
            throw new InvalidArgumentValue('sortOrder', $locationUpdateStruct->sortOrder, 'LocationUpdateStruct');
533
        }
534
535
        $loadedLocation = $this->loadLocation($location->id);
536
537
        if ($locationUpdateStruct->remoteId !== null) {
538
            try {
539
                $existingLocation = $this->loadLocationByRemoteId($locationUpdateStruct->remoteId);
540
                if ($existingLocation !== null && $existingLocation->id !== $loadedLocation->id) {
541
                    throw new InvalidArgumentException('locationUpdateStruct', 'Location with the provided remote ID already exists');
542
                }
543
            } catch (APINotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
544
            }
545
        }
546
547
        if (!$this->permissionResolver->canUser('content', 'edit', $loadedLocation->getContentInfo(), [$loadedLocation])) {
548
            throw new UnauthorizedException('content', 'edit', ['locationId' => $loadedLocation->id]);
549
        }
550
551
        $updateStruct = new UpdateStruct();
552
        $updateStruct->priority = $locationUpdateStruct->priority !== null ? $locationUpdateStruct->priority : $loadedLocation->priority;
553
        $updateStruct->remoteId = $locationUpdateStruct->remoteId !== null ? trim($locationUpdateStruct->remoteId) : $loadedLocation->remoteId;
554
        $updateStruct->sortField = $locationUpdateStruct->sortField !== null ? $locationUpdateStruct->sortField : $loadedLocation->sortField;
555
        $updateStruct->sortOrder = $locationUpdateStruct->sortOrder !== null ? $locationUpdateStruct->sortOrder : $loadedLocation->sortOrder;
556
557
        $this->repository->beginTransaction();
558
        try {
559
            $this->persistenceHandler->locationHandler()->update($updateStruct, $loadedLocation->id);
560
            $this->repository->commit();
561
        } catch (Exception $e) {
562
            $this->repository->rollback();
563
            throw $e;
564
        }
565
566
        return $this->loadLocation($loadedLocation->id);
567
    }
568
569
    /**
570
     * Swaps the contents held by $location1 and $location2.
571
     *
572
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to swap content
573
     *
574
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location1
575
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location2
576
     */
577
    public function swapLocation(APILocation $location1, APILocation $location2): void
578
    {
579
        $loadedLocation1 = $this->loadLocation($location1->id);
580
        $loadedLocation2 = $this->loadLocation($location2->id);
581
582
        if (!$this->permissionResolver->canUser('content', 'edit', $loadedLocation1->getContentInfo(), [$loadedLocation1])) {
583
            throw new UnauthorizedException('content', 'edit', ['locationId' => $loadedLocation1->id]);
584
        }
585
        if (!$this->permissionResolver->canUser('content', 'edit', $loadedLocation2->getContentInfo(), [$loadedLocation2])) {
586
            throw new UnauthorizedException('content', 'edit', ['locationId' => $loadedLocation2->id]);
587
        }
588
589
        $this->repository->beginTransaction();
590
        try {
591
            $this->persistenceHandler->locationHandler()->swap($loadedLocation1->id, $loadedLocation2->id);
592
            $this->persistenceHandler->urlAliasHandler()->locationSwapped(
593
                $location1->id,
594
                $location1->parentLocationId,
595
                $location2->id,
596
                $location2->parentLocationId
597
            );
598
            $this->persistenceHandler->bookmarkHandler()->locationSwapped($loadedLocation1->id, $loadedLocation2->id);
599
            $this->repository->commit();
600
        } catch (Exception $e) {
601
            $this->repository->rollback();
602
            throw $e;
603
        }
604
    }
605
606
    /**
607
     * Hides the $location and marks invisible all descendants of $location.
608
     *
609
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to hide this location
610
     *
611
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
612
     *
613
     * @return \eZ\Publish\API\Repository\Values\Content\Location $location, with updated hidden value
614
     */
615
    public function hideLocation(APILocation $location): APILocation
616
    {
617
        if (!$this->permissionResolver->canUser('content', 'hide', $location->getContentInfo(), [$location])) {
618
            throw new UnauthorizedException('content', 'hide', ['locationId' => $location->id]);
619
        }
620
621
        $this->repository->beginTransaction();
622
        try {
623
            $this->persistenceHandler->locationHandler()->hide($location->id);
624
            $this->repository->commit();
625
        } catch (Exception $e) {
626
            $this->repository->rollback();
627
            throw $e;
628
        }
629
630
        return $this->loadLocation($location->id);
631
    }
632
633
    /**
634
     * Unhides the $location.
635
     *
636
     * This method and marks visible all descendants of $locations
637
     * until a hidden location is found.
638
     *
639
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to unhide this location
640
     *
641
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
642
     *
643
     * @return \eZ\Publish\API\Repository\Values\Content\Location $location, with updated hidden value
644
     */
645
    public function unhideLocation(APILocation $location): APILocation
646
    {
647
        if (!$this->permissionResolver->canUser('content', 'hide', $location->getContentInfo(), [$location])) {
648
            throw new UnauthorizedException('content', 'hide', ['locationId' => $location->id]);
649
        }
650
651
        $this->repository->beginTransaction();
652
        try {
653
            $this->persistenceHandler->locationHandler()->unHide($location->id);
654
            $this->repository->commit();
655
        } catch (Exception $e) {
656
            $this->repository->rollback();
657
            throw $e;
658
        }
659
660
        return $this->loadLocation($location->id);
661
    }
662
663
    /**
664
     * Moves the subtree to $newParentLocation.
665
     *
666
     * If a user has the permission to move the location to a target location
667
     * he can do it regardless of an existing descendant on which the user has no permission.
668
     *
669
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed to move this location to the target
670
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user does not have read access to the whole source subtree
671
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException If the new parent is in a subtree of the location
672
     *
673
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
674
     * @param \eZ\Publish\API\Repository\Values\Content\Location $newParentLocation
675
     */
676
    public function moveSubtree(APILocation $location, APILocation $newParentLocation): void
677
    {
678
        $location = $this->loadLocation($location->id);
679
        $newParentLocation = $this->loadLocation($newParentLocation->id);
680
681
        // check create permission on target location
682
        if (!$this->permissionResolver->canUser('content', 'create', $location->getContentInfo(), [$newParentLocation])) {
683
            throw new UnauthorizedException('content', 'create', ['locationId' => $newParentLocation->id]);
684
        }
685
686
        /** Check read access to whole source subtree
687
         * @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
688
         */
689
        $contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'read');
690
        if ($contentReadCriterion === false) {
691
            throw new UnauthorizedException('content', 'read');
692
        } elseif ($contentReadCriterion !== true) {
693
            // Query if there are any content in subtree current user don't have access to
694
            $query = new Query(
695
                [
696
                    'limit' => 0,
697
                    'filter' => new CriterionLogicalAnd(
698
                        [
699
                            new CriterionSubtree($location->pathString),
700
                            new CriterionLogicalNot($contentReadCriterion),
0 ignored issues
show
Bug introduced by
It seems like $contentReadCriterion defined by $this->permissionCriteri...rion('content', 'read') on line 689 can also be of type boolean; however, eZ\Publish\API\Repositor...gicalNot::__construct() does only seem to accept object<eZ\Publish\API\Re...ontent\Query\Criterion>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
701
                        ]
702
                    ),
703
                ]
704
            );
705
            $result = $this->repository->getSearchService()->findContent($query, [], false);
706
            if ($result->totalCount > 0) {
707
                throw new UnauthorizedException('content', 'read');
708
            }
709
        }
710
711
        if (strpos($newParentLocation->pathString, $location->pathString) === 0) {
712
            throw new InvalidArgumentException(
713
                '$newParentLocation',
714
                'new parent Location is a descendant of the given $location'
715
            );
716
        }
717
718
        $this->repository->beginTransaction();
719
        try {
720
            $this->persistenceHandler->locationHandler()->move($location->id, $newParentLocation->id);
721
722
            $content = $this->repository->getContentService()->loadContent($location->contentId);
723
            $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
724
            foreach ($urlAliasNames as $languageCode => $name) {
725
                $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
726
                    $location->id,
727
                    $newParentLocation->id,
728
                    $name,
729
                    $languageCode,
730
                    $content->contentInfo->alwaysAvailable
731
                );
732
            }
733
734
            $this->persistenceHandler->urlAliasHandler()->locationMoved(
735
                $location->id,
736
                $location->parentLocationId,
737
                $newParentLocation->id
738
            );
739
740
            $this->repository->commit();
741
        } catch (Exception $e) {
742
            $this->repository->rollback();
743
            throw $e;
744
        }
745
    }
746
747
    /**
748
     * Deletes $location and all its descendants.
749
     *
750
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user is not allowed to delete this location or a descendant
751
     *
752
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
753
     */
754
    public function deleteLocation(APILocation $location): void
755
    {
756
        $location = $this->loadLocation($location->id);
757
758
        if (!$this->permissionResolver->canUser('content', 'manage_locations', $location->getContentInfo())) {
759
            throw new UnauthorizedException('content', 'manage_locations', ['locationId' => $location->id]);
760
        }
761
        if (!$this->permissionResolver->canUser('content', 'remove', $location->getContentInfo(), [$location])) {
762
            throw new UnauthorizedException('content', 'remove', ['locationId' => $location->id]);
763
        }
764
765
        /** Check remove access to descendants
766
         * @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
767
         */
768
        $contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'remove');
769
        if ($contentReadCriterion === false) {
770
            throw new UnauthorizedException('content', 'remove');
771
        } elseif ($contentReadCriterion !== true) {
772
            // Query if there are any content in subtree current user don't have access to
773
            $query = new Query(
774
                [
775
                    'limit' => 0,
776
                    'filter' => new CriterionLogicalAnd(
777
                        [
778
                            new CriterionSubtree($location->pathString),
779
                            new CriterionLogicalNot($contentReadCriterion),
0 ignored issues
show
Bug introduced by
It seems like $contentReadCriterion defined by $this->permissionCriteri...on('content', 'remove') on line 768 can also be of type boolean; however, eZ\Publish\API\Repositor...gicalNot::__construct() does only seem to accept object<eZ\Publish\API\Re...ontent\Query\Criterion>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
780
                        ]
781
                    ),
782
                ]
783
            );
784
            $result = $this->repository->getSearchService()->findContent($query, [], false);
785
            if ($result->totalCount > 0) {
786
                throw new UnauthorizedException('content', 'remove');
787
            }
788
        }
789
790
        $this->repository->beginTransaction();
791
        try {
792
            $this->persistenceHandler->locationHandler()->removeSubtree($location->id);
793
            $this->persistenceHandler->urlAliasHandler()->locationDeleted($location->id);
794
            $this->repository->commit();
795
        } catch (Exception $e) {
796
            $this->repository->rollback();
797
            throw $e;
798
        }
799
    }
800
801
    /**
802
     * Instantiates a new location create class.
803
     *
804
     * @param mixed $parentLocationId the parent under which the new location should be created
805
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|null $contentType
806
     *
807
     * @return \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct
808
     */
809
    public function newLocationCreateStruct($parentLocationId, ContentType $contentType = null): LocationCreateStruct
810
    {
811
        $properties = [
812
            'parentLocationId' => $parentLocationId,
813
        ];
814
        if ($contentType) {
815
            $properties['sortField'] = $contentType->defaultSortField;
816
            $properties['sortOrder'] = $contentType->defaultSortOrder;
817
        }
818
819
        return new LocationCreateStruct($properties);
820
    }
821
822
    /**
823
     * Instantiates a new location update class.
824
     *
825
     * @return \eZ\Publish\API\Repository\Values\Content\LocationUpdateStruct
826
     */
827
    public function newLocationUpdateStruct(): LocationUpdateStruct
828
    {
829
        return new LocationUpdateStruct();
830
    }
831
832
    /**
833
     * Get the total number of all existing Locations. Can be combined with loadAllLocations.
834
     *
835
     * @see loadAllLocations
836
     *
837
     * @return int Total number of Locations
838
     */
839
    public function getAllLocationsCount(): int
840
    {
841
        return $this->persistenceHandler->locationHandler()->countAllLocations();
842
    }
843
844
    /**
845
     * Bulk-load all existing Locations, constrained by $limit and $offset to paginate results.
846
     *
847
     * @param int $offset
848
     * @param int $limit
849
     *
850
     * @return \eZ\Publish\API\Repository\Values\Content\Location[]
851
     *
852
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
853
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
854
     */
855
    public function loadAllLocations(int $offset = 0, int $limit = 25): array
856
    {
857
        $spiLocations = $this->persistenceHandler->locationHandler()->loadAllLocations(
858
            $offset,
859
            $limit
860
        );
861
        $contentIds = array_unique(
862
            array_map(
863
                function (SPILocation $spiLocation) {
864
                    return $spiLocation->contentId;
865
                },
866
                $spiLocations
867
            )
868
        );
869
870
        $permissionResolver = $this->repository->getPermissionResolver();
871
        $spiContentInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList(
872
            $contentIds
873
        );
874
        $contentList = $this->contentDomainMapper->buildContentProxyList(
875
            $spiContentInfoList,
876
            Language::ALL,
877
            false
878
        );
879
        $locations = [];
880
        foreach ($spiLocations as $spiLocation) {
881
            if (!isset($spiContentInfoList[$spiLocation->contentId], $contentList[$spiLocation->contentId])) {
882
                $this->logger->warning(
883
                    sprintf(
884
                        'Location %d has missing content %d',
885
                        $spiLocation->id,
886
                        $spiLocation->contentId
887
                    )
888
                );
889
                continue;
890
            }
891
892
            $location = $this->contentDomainMapper->buildLocationWithContent(
893
                $spiLocation,
894
                $contentList[$spiLocation->contentId],
895
                $spiContentInfoList[$spiLocation->contentId]
896
            );
897
898
            $contentInfo = $location->getContentInfo();
899
            if (!$permissionResolver->canUser('content', 'read', $contentInfo, [$location])) {
900
                continue;
901
            }
902
            $locations[] = $location;
903
        }
904
905
        return $locations;
906
    }
907
}
908