Completed
Push — ezp-30882-thumbnail ( 274ed9...d4335b )
by
unknown
14:43
created

LocationService::createLocation()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 20
nop 2
dl 0
loc 74
rs 7.0117
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
 * File containing the eZ\Publish\Core\Repository\LocationService 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\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\LocationUpdateStruct;
15
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
16
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
17
use eZ\Publish\API\Repository\Values\Content\Location as APILocation;
18
use eZ\Publish\API\Repository\Values\Content\LocationList;
19
use eZ\Publish\API\Repository\Values\Content\VersionInfo;
20
use eZ\Publish\SPI\Persistence\Content\Location as SPILocation;
21
use eZ\Publish\SPI\Persistence\Content\Location\UpdateStruct;
22
use eZ\Publish\API\Repository\LocationService as LocationServiceInterface;
23
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
24
use eZ\Publish\SPI\Persistence\Handler;
25
use eZ\Publish\API\Repository\Values\Content\Query;
26
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
27
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
28
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd as CriterionLogicalAnd;
29
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalNot as CriterionLogicalNot;
30
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Subtree as CriterionSubtree;
31
use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException;
32
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
33
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
34
use eZ\Publish\Core\Base\Exceptions\BadStateException;
35
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
36
use Exception;
37
use Psr\Log\LoggerInterface;
38
use Psr\Log\NullLogger;
39
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
40
41
/**
42
 * Location service, used for complex subtree operations.
43
 *
44
 * @example Examples/location.php
45
 */
46
class LocationService implements LocationServiceInterface
47
{
48
    /** @var \eZ\Publish\Core\Repository\Repository */
49
    protected $repository;
50
51
    /** @var \eZ\Publish\SPI\Persistence\Handler */
52
    protected $persistenceHandler;
53
54
    /** @var array */
55
    protected $settings;
56
57
    /** @var \eZ\Publish\Core\Repository\Helper\DomainMapper */
58
    protected $domainMapper;
59
60
    /** @var \eZ\Publish\Core\Repository\Helper\NameSchemaService */
61
    protected $nameSchemaService;
62
63
    /** @var \eZ\Publish\API\Repository\PermissionCriterionResolver */
64
    protected $permissionCriterionResolver;
65
66
    /** @var \Psr\Log\LoggerInterface */
67
    private $logger;
68
69
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
70
    private $permissionResolver;
71
72
    /**
73
     * Setups service with reference to repository object that created it & corresponding handler.
74
     *
75
     * @param \eZ\Publish\API\Repository\Repository $repository
76
     * @param \eZ\Publish\SPI\Persistence\Handler $handler
77
     * @param \eZ\Publish\Core\Repository\Helper\DomainMapper $domainMapper
78
     * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
79
     * @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver
80
     * @param array $settings
81
     * @param \Psr\Log\LoggerInterface|null $logger
82
     */
83
    public function __construct(
84
        RepositoryInterface $repository,
85
        Handler $handler,
86
        Helper\DomainMapper $domainMapper,
87
        Helper\NameSchemaService $nameSchemaService,
88
        PermissionCriterionResolver $permissionCriterionResolver,
89
        PermissionResolver $permissionResolver,
90
        array $settings = [],
91
        LoggerInterface $logger = null
92
    ) {
93
        $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...
94
        $this->persistenceHandler = $handler;
95
        $this->domainMapper = $domainMapper;
96
        $this->nameSchemaService = $nameSchemaService;
97
        $this->permissionResolver = $permissionResolver;
98
        // Union makes sure default settings are ignored if provided in argument
99
        $this->settings = $settings + [
100
            //'defaultSetting' => array(),
101
        ];
102
        $this->permissionCriterionResolver = $permissionCriterionResolver;
103
        $this->logger = null !== $logger ? $logger : new NullLogger();
104
    }
105
106
    /**
107
     * Copies the subtree starting from $subtree as a new subtree of $targetLocation.
108
     *
109
     * Only the items on which the user has read access are copied.
110
     *
111
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user is not allowed copy the subtree to the given parent location
112
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user user does not have read access to the whole source subtree
113
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the target location is a sub location of the given location
114
     *
115
     * @param \eZ\Publish\API\Repository\Values\Content\Location $subtree - the subtree denoted by the location to copy
116
     * @param \eZ\Publish\API\Repository\Values\Content\Location $targetParentLocation - the target parent location for the copy operation
117
     *
118
     * @return \eZ\Publish\API\Repository\Values\Content\Location The newly created location of the copied subtree
119
     */
120
    public function copySubtree(APILocation $subtree, APILocation $targetParentLocation)
121
    {
122
        $loadedSubtree = $this->loadLocation($subtree->id);
123
        $loadedTargetLocation = $this->loadLocation($targetParentLocation->id);
124
125
        if (stripos($loadedTargetLocation->pathString, $loadedSubtree->pathString) !== false) {
126
            throw new InvalidArgumentException('targetParentLocation', 'target parent location is a sub location of the given subtree');
127
        }
128
129
        // check create permission on target
130
        if (!$this->permissionResolver->canUser('content', 'create', $loadedSubtree->getContentInfo(), [$loadedTargetLocation])) {
131
            throw new UnauthorizedException('content', 'create', ['locationId' => $loadedTargetLocation->id]);
132
        }
133
134
        /** Check read access to whole source subtree
135
         * @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
136
         */
137
        $contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion();
0 ignored issues
show
Bug introduced by
The call to getPermissionsCriterion() misses some required arguments starting with $module.
Loading history...
138 View Code Duplication
        if ($contentReadCriterion === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
139
            throw new UnauthorizedException('content', 'read');
140
        } elseif ($contentReadCriterion !== true) {
141
            // Query if there are any content in subtree current user don't have access to
142
            $query = new Query(
143
                [
144
                    'limit' => 0,
145
                    'filter' => new CriterionLogicalAnd(
146
                        [
147
                            new CriterionSubtree($loadedSubtree->pathString),
148
                            new CriterionLogicalNot($contentReadCriterion),
0 ignored issues
show
Bug introduced by
It seems like $contentReadCriterion defined by $this->permissionCriteri...tPermissionsCriterion() on line 137 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...
149
                        ]
150
                    ),
151
                ]
152
            );
153
            $result = $this->repository->getSearchService()->findContent($query, [], false);
154
            if ($result->totalCount > 0) {
155
                throw new UnauthorizedException('content', 'read');
156
            }
157
        }
158
159
        $this->repository->beginTransaction();
160
        try {
161
            $newLocation = $this->persistenceHandler->locationHandler()->copySubtree(
162
                $loadedSubtree->id,
163
                $loadedTargetLocation->id,
164
                $this->repository->getPermissionResolver()->getCurrentUserReference()->getUserId()
165
            );
166
167
            $content = $this->repository->getContentService()->loadContent($newLocation->contentId);
168
            $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
169 View Code Duplication
            foreach ($urlAliasNames as $languageCode => $name) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
170
                $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
171
                    $newLocation->id,
172
                    $loadedTargetLocation->id,
173
                    $name,
174
                    $languageCode,
175
                    $content->contentInfo->alwaysAvailable
176
                );
177
            }
178
179
            $this->persistenceHandler->urlAliasHandler()->locationCopied(
180
                $loadedSubtree->id,
181
                $newLocation->id,
182
                $loadedTargetLocation->id
183
            );
184
185
            $this->repository->commit();
186
        } catch (Exception $e) {
187
            $this->repository->rollback();
188
            throw $e;
189
        }
190
191
        return $this->domainMapper->buildLocationWithContent($newLocation, $content);
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    public function loadLocation($locationId, array $prioritizedLanguages = null, bool $useAlwaysAvailable = null)
198
    {
199
        $spiLocation = $this->persistenceHandler->locationHandler()->load($locationId, $prioritizedLanguages, $useAlwaysAvailable ?? true);
0 ignored issues
show
Bug introduced by
It seems like $prioritizedLanguages defined by parameter $prioritizedLanguages on line 197 can also be of type array; however, eZ\Publish\SPI\Persisten...ocation\Handler::load() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
200
        $location = $this->domainMapper->buildLocation($spiLocation, $prioritizedLanguages ?: [], $useAlwaysAvailable ?? true);
201
        if (!$this->permissionResolver->canUser('content', 'read', $location->getContentInfo(), [$location])) {
202
            throw new UnauthorizedException('content', 'read', ['locationId' => $location->id]);
203
        }
204
205
        return $location;
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function loadLocationList(array $locationIds, array $prioritizedLanguages = null, bool $useAlwaysAvailable = null): iterable
212
    {
213
        $spiLocations = $this->persistenceHandler->locationHandler()->loadList(
214
            $locationIds,
215
            $prioritizedLanguages,
0 ignored issues
show
Bug introduced by
It seems like $prioritizedLanguages defined by parameter $prioritizedLanguages on line 211 can also be of type array; however, eZ\Publish\SPI\Persisten...ion\Handler::loadList() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
216
            $useAlwaysAvailable ?? true
217
        );
218
        if (empty($spiLocations)) {
219
            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...
220
        }
221
222
        // Get content id's
223
        $contentIds = [];
224
        foreach ($spiLocations as $spiLocation) {
225
            $contentIds[] = $spiLocation->contentId;
226
        }
227
228
        // Load content info and Get content proxy
229
        $spiContentInfoList = $this->persistenceHandler->contentHandler()->loadContentInfoList($contentIds);
230
        $contentProxyList = $this->domainMapper->buildContentProxyList(
231
            $spiContentInfoList,
232
            $prioritizedLanguages ?? [],
233
            $useAlwaysAvailable ?? true
234
        );
235
236
        // Build locations using the bulk retrieved content info and bulk lazy loaded content proxies.
237
        $locations = [];
238
        $permissionResolver = $this->repository->getPermissionResolver();
239
        foreach ($spiLocations as $spiLocation) {
240
            $location = $this->domainMapper->buildLocationWithContent(
241
                $spiLocation,
242
                $contentProxyList[$spiLocation->contentId] ?? null,
243
                $spiContentInfoList[$spiLocation->contentId] ?? null
244
            );
245
246
            if ($permissionResolver->canUser('content', 'read', $location->getContentInfo(), [$location])) {
247
                $locations[$spiLocation->id] = $location;
248
            }
249
        }
250
251
        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...
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257
    public function loadLocationByRemoteId($remoteId, array $prioritizedLanguages = null, bool $useAlwaysAvailable = null)
258
    {
259
        if (!is_string($remoteId)) {
260
            throw new InvalidArgumentValue('remoteId', $remoteId);
261
        }
262
263
        $spiLocation = $this->persistenceHandler->locationHandler()->loadByRemoteId($remoteId, $prioritizedLanguages, $useAlwaysAvailable ?? true);
0 ignored issues
show
Bug introduced by
It seems like $prioritizedLanguages defined by parameter $prioritizedLanguages on line 257 can also be of type array; however, eZ\Publish\SPI\Persisten...ndler::loadByRemoteId() does only seem to accept null|array<integer,string>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
686
            throw new UnauthorizedException('content', 'read');
687
        } elseif ($contentReadCriterion !== true) {
688
            // Query if there are any content in subtree current user don't have access to
689
            $query = new Query(
690
                [
691
                    'limit' => 0,
692
                    'filter' => new CriterionLogicalAnd(
693
                        [
694
                            new CriterionSubtree($location->pathString),
695
                            new CriterionLogicalNot($contentReadCriterion),
0 ignored issues
show
Bug introduced by
It seems like $contentReadCriterion defined by $this->permissionCriteri...tPermissionsCriterion() on line 684 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...
696
                        ]
697
                    ),
698
                ]
699
            );
700
            $result = $this->repository->getSearchService()->findContent($query, [], false);
701
            if ($result->totalCount > 0) {
702
                throw new UnauthorizedException('content', 'read');
703
            }
704
        }
705
706
        if (strpos($newParentLocation->pathString, $location->pathString) === 0) {
707
            throw new InvalidArgumentException(
708
                '$newParentLocation',
709
                'new parent location is in a subtree of the given $location'
710
            );
711
        }
712
713
        $this->repository->beginTransaction();
714
        try {
715
            $this->persistenceHandler->locationHandler()->move($location->id, $newParentLocation->id);
716
717
            $content = $this->repository->getContentService()->loadContent($location->contentId);
718
            $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
719 View Code Duplication
            foreach ($urlAliasNames as $languageCode => $name) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
720
                $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
721
                    $location->id,
722
                    $newParentLocation->id,
723
                    $name,
724
                    $languageCode,
725
                    $content->contentInfo->alwaysAvailable
726
                );
727
            }
728
729
            $this->persistenceHandler->urlAliasHandler()->locationMoved(
730
                $location->id,
731
                $location->parentLocationId,
732
                $newParentLocation->id
733
            );
734
735
            $this->repository->commit();
736
        } catch (Exception $e) {
737
            $this->repository->rollback();
738
            throw $e;
739
        }
740
    }
741
742
    /**
743
     * Deletes $location and all its descendants.
744
     *
745
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException If the current user is not allowed to delete this location or a descendant
746
     *
747
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
748
     */
749
    public function deleteLocation(APILocation $location)
750
    {
751
        $location = $this->loadLocation($location->id);
752
753
        if (!$this->permissionResolver->canUser('content', 'manage_locations', $location->getContentInfo())) {
754
            throw new UnauthorizedException('content', 'manage_locations', ['locationId' => $location->id]);
755
        }
756
        if (!$this->permissionResolver->canUser('content', 'remove', $location->getContentInfo(), [$location])) {
757
            throw new UnauthorizedException('content', 'remove', ['locationId' => $location->id]);
758
        }
759
760
        /** Check remove access to descendants
761
         * @var bool|\eZ\Publish\API\Repository\Values\Content\Query\Criterion
762
         */
763
        $contentReadCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'remove');
764 View Code Duplication
        if ($contentReadCriterion === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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