Completed
Push — in-memory-cache2 ( b8bac7...8ddfe2 )
by André
43:28 queued 23:26
created

TrashService::userHasPermissionsToRemove()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 24
rs 9.536
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the eZ\Publish\Core\Repository\TrashService 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\TrashService as TrashServiceInterface;
12
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
13
use eZ\Publish\API\Repository\Values\Content\Content;
14
use eZ\Publish\API\Repository\Exceptions\UnauthorizedException as APIUnauthorizedException;
15
use eZ\Publish\SPI\Persistence\Handler;
16
use eZ\Publish\API\Repository\Values\Content\Location;
17
use eZ\Publish\Core\Repository\Values\Content\TrashItem;
18
use eZ\Publish\API\Repository\Values\Content\TrashItem as APITrashItem;
19
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
20
use eZ\Publish\API\Repository\Values\Content\Query;
21
use eZ\Publish\SPI\Persistence\Content\Location\Trashed;
22
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
23
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
24
use eZ\Publish\API\Repository\Values\Content\Trash\SearchResult;
25
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
26
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
27
use eZ\Publish\API\Repository\PermissionCriterionResolver;
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 DateTime;
32
use Exception;
33
34
/**
35
 * Trash service, used for managing trashed content.
36
 */
37
class TrashService implements TrashServiceInterface
38
{
39
    /**
40
     * @var \eZ\Publish\Core\Repository\Repository
41
     */
42
    protected $repository;
43
44
    /**
45
     * @var \eZ\Publish\SPI\Persistence\Handler
46
     */
47
    protected $persistenceHandler;
48
49
    /**
50
     * @var array
51
     */
52
    protected $settings;
53
54
    /**
55
     * @var \eZ\Publish\Core\Repository\Helper\NameSchemaService
56
     */
57
    protected $nameSchemaService;
58
59
    /**
60
     * Setups service with reference to repository object that created it & corresponding handler.
61
     *
62
     * @param \eZ\Publish\API\Repository\Repository $repository
63
     * @param \eZ\Publish\SPI\Persistence\Handler $handler
64
     * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService
65
     * @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver
66
     * @param array $settings
67
     */
68
    public function __construct(
69
        RepositoryInterface $repository,
70
        Handler $handler,
71
        Helper\NameSchemaService $nameSchemaService,
72
        PermissionCriterionResolver $permissionCriterionResolver,
73
        array $settings = array()
74
    ) {
75
        $this->permissionCriterionResolver = $permissionCriterionResolver;
0 ignored issues
show
Bug introduced by
The property permissionCriterionResolver does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
76
        $this->repository = $repository;
0 ignored issues
show
Documentation Bug introduced by
$repository is of type object<eZ\Publish\API\Repository\Repository>, but the property $repository was declared to be of type object<eZ\Publish\Core\Repository\Repository>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
77
        $this->persistenceHandler = $handler;
78
        $this->nameSchemaService = $nameSchemaService;
79
        // Union makes sure default settings are ignored if provided in argument
80
        $this->settings = $settings + array(
81
            //'defaultSetting' => array(),
82
        );
83
    }
84
85
    /**
86
     * Loads a trashed location object from its $id.
87
     *
88
     * Note that $id is identical to original location, which has been previously trashed
89
     *
90
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to read the trashed location
91
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - if the location with the given id does not exist
92
     *
93
     * @param mixed $trashItemId
94
     *
95
     * @return \eZ\Publish\API\Repository\Values\Content\TrashItem
96
     */
97
    public function loadTrashItem($trashItemId)
98
    {
99
        $spiTrashItem = $this->persistenceHandler->trashHandler()->loadTrashItem($trashItemId);
100
        $trash = $this->buildDomainTrashItemObject(
101
            $spiTrashItem,
102
            $this->repository->getContentService()->internalLoadContent($spiTrashItem->contentId)
0 ignored issues
show
Bug introduced by
The method internalLoadContent() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
103
        );
104
        if (!$this->repository->canUser('content', 'read', $trash->getContentInfo())) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

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

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

Loading history...
105
            throw new UnauthorizedException('content', 'read');
106
        }
107
108
        if (!$this->repository->canUser('content', 'restore', $trash->getContentInfo())) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

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

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

Loading history...
109
            throw new UnauthorizedException('content', 'restore');
110
        }
111
112
        return $trash;
113
    }
114
115
    /**
116
     * Sends $location and all its children to trash and returns the corresponding trash item.
117
     *
118
     * The current user may not have access to the returned trash item, check before using it.
119
     * Content is left untouched.
120
     *
121
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to trash the given location
122
     *
123
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
124
     *
125
     * @return null|\eZ\Publish\API\Repository\Values\Content\TrashItem null if location was deleted, otherwise TrashItem
126
     */
127
    public function trash(Location $location)
128
    {
129
        if (empty($location->id)) {
130
            throw new InvalidArgumentValue('id', $location->id, 'Location');
131
        }
132
133
        if (!$this->userHasPermissionsToRemove($location->getContentInfo(), $location)) {
134
            throw new UnauthorizedException('content', 'remove');
135
        }
136
137
        $this->repository->beginTransaction();
138
        try {
139
            $spiTrashItem = $this->persistenceHandler->trashHandler()->trashSubtree($location->id);
140
            $this->persistenceHandler->urlAliasHandler()->locationDeleted($location->id);
141
            $this->repository->commit();
142
        } catch (Exception $e) {
143
            $this->repository->rollback();
144
            throw $e;
145
        }
146
147
        // Use internalLoadContent() as we want a trash item regardless of user access to the trash or not.
148
        try {
149
            return isset($spiTrashItem)
150
                ? $this->buildDomainTrashItemObject(
151
                    $spiTrashItem,
152
                    $this->repository->getContentService()->internalLoadContent($spiTrashItem->contentId)
0 ignored issues
show
Bug introduced by
The method internalLoadContent() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
153
                )
154
                : null;
155
        } catch (Exception $e) {
156
            return null;
157
        }
158
    }
159
160
    /**
161
     * Recovers the $trashedLocation at its original place if possible.
162
     *
163
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to recover the trash item at the parent location location
164
     *
165
     * If $newParentLocation is provided, $trashedLocation will be restored under it.
166
     *
167
     * @param \eZ\Publish\API\Repository\Values\Content\TrashItem $trashItem
168
     * @param \eZ\Publish\API\Repository\Values\Content\Location $newParentLocation
169
     *
170
     * @return \eZ\Publish\API\Repository\Values\Content\Location the newly created or recovered location
171
     */
172
    public function recover(APITrashItem $trashItem, Location $newParentLocation = null)
173
    {
174
        if (!is_numeric($trashItem->id)) {
175
            throw new InvalidArgumentValue('id', $trashItem->id, 'TrashItem');
176
        }
177
178
        if ($newParentLocation === null && !is_numeric($trashItem->parentLocationId)) {
179
            throw new InvalidArgumentValue('parentLocationId', $trashItem->parentLocationId, 'TrashItem');
180
        }
181
182
        if ($newParentLocation !== null && !is_numeric($newParentLocation->id)) {
183
            throw new InvalidArgumentValue('parentLocationId', $newParentLocation->id, 'Location');
184
        }
185
186
        if (!$this->repository->canUser(
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

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

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

Loading history...
187
            'content',
188
            'restore',
189
            $trashItem->getContentInfo(),
190
            [$newParentLocation ?: $trashItem]
191
        )) {
192
            throw new UnauthorizedException('content', 'restore');
193
        }
194
195
        $this->repository->beginTransaction();
196
        try {
197
            $newParentLocationId = $newParentLocation ? $newParentLocation->id : $trashItem->parentLocationId;
198
            $newLocationId = $this->persistenceHandler->trashHandler()->recover(
199
                $trashItem->id,
200
                $newParentLocationId
201
            );
202
203
            $content = $this->repository->getContentService()->loadContent($trashItem->contentId);
204
            $urlAliasNames = $this->nameSchemaService->resolveUrlAliasSchema($content);
205
206
            // Publish URL aliases for recovered location
207
            foreach ($urlAliasNames as $languageCode => $name) {
208
                $this->persistenceHandler->urlAliasHandler()->publishUrlAliasForLocation(
209
                    $newLocationId,
210
                    $newParentLocationId,
211
                    $name,
212
                    $languageCode,
213
                    $content->contentInfo->alwaysAvailable
214
                );
215
            }
216
217
            $this->repository->commit();
218
        } catch (Exception $e) {
219
            $this->repository->rollback();
220
            throw $e;
221
        }
222
223
        return $this->repository->getLocationService()->loadLocation($newLocationId);
224
    }
225
226
    /**
227
     * Empties trash.
228
     *
229
     * All locations contained in the trash will be removed. Content objects will be removed
230
     * if all locations of the content are gone.
231
     *
232
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to empty the trash
233
     */
234
    public function emptyTrash()
235
    {
236
        // Will throw if you have Role assignment limitation where you have content/cleantrash permission.
237
        // This is by design and means you can only delete one and one trash item, or you'll need to change how
238
        // permissions is assigned to be on separate role with no Role assignment limitation.
239
        if ($this->repository->hasAccess('content', 'cleantrash') !== true) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::hasAccess() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::hasAccess() instead. Check if user has access to a given module / function. Low level function, use canUser instead if you have objects to check against.

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

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

Loading history...
240
            throw new UnauthorizedException('content', 'cleantrash');
241
        }
242
243
        $this->repository->beginTransaction();
244
        try {
245
            // Persistence layer takes care of deleting content objects
246
            $result = $this->persistenceHandler->trashHandler()->emptyTrash();
247
            $this->repository->commit();
248
249
            return $result;
250
        } catch (Exception $e) {
251
            $this->repository->rollback();
252
            throw $e;
253
        }
254
    }
255
256
    /**
257
     * Deletes a trash item.
258
     *
259
     * The corresponding content object will be removed
260
     *
261
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the user is not allowed to delete this trash item
262
     *
263
     * @param \eZ\Publish\API\Repository\Values\Content\TrashItem $trashItem
264
     *
265
     * @return \eZ\Publish\API\Repository\Values\Content\Trash\TrashItemDeleteResult
266
     */
267 View Code Duplication
    public function deleteTrashItem(APITrashItem $trashItem)
268
    {
269
        if (!$this->repository->canUser('content', 'cleantrash', $trashItem->getContentInfo())) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

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

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

Loading history...
270
            throw new UnauthorizedException('content', 'cleantrash');
271
        }
272
273
        if (!is_numeric($trashItem->id)) {
274
            throw new InvalidArgumentValue('id', $trashItem->id, 'TrashItem');
275
        }
276
277
        $this->repository->beginTransaction();
278
        try {
279
            $trashItemDeleteResult = $this->persistenceHandler->trashHandler()->deleteTrashItem($trashItem->id);
280
            $this->repository->commit();
281
282
            return $trashItemDeleteResult;
283
        } catch (Exception $e) {
284
            $this->repository->rollback();
285
            throw $e;
286
        }
287
    }
288
289
    /**
290
     * Returns a collection of Trashed locations contained in the trash, which are readable by the current user.
291
     *
292
     * $query allows to filter/sort the elements to be contained in the collection.
293
     *
294
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
295
     *
296
     * @return \eZ\Publish\API\Repository\Values\Content\Trash\SearchResult
297
     */
298
    public function findTrashItems(Query $query)
299
    {
300
        if ($query->filter !== null && !$query->filter instanceof Criterion) {
301
            throw new InvalidArgumentValue('query->filter', $query->filter, 'Query');
302
        }
303
304
        if ($query->sortClauses !== null) {
305
            if (!is_array($query->sortClauses)) {
306
                throw new InvalidArgumentValue('query->sortClauses', $query->sortClauses, 'Query');
307
            }
308
309
            foreach ($query->sortClauses as $sortClause) {
310
                if (!$sortClause instanceof SortClause) {
311
                    throw new InvalidArgumentValue('query->sortClauses', 'only instances of SortClause class are allowed');
312
                }
313
            }
314
        }
315
316 View Code Duplication
        if ($query->offset !== null && !is_numeric($query->offset)) {
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...
317
            throw new InvalidArgumentValue('query->offset', $query->offset, 'Query');
318
        }
319
320 View Code Duplication
        if ($query->limit !== null && !is_numeric($query->limit)) {
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...
321
            throw new InvalidArgumentValue('query->limit', $query->limit, 'Query');
322
        }
323
324
        $spiTrashItems = $this->persistenceHandler->trashHandler()->findTrashItems(
325
            $query->filter,
326
            $query->offset !== null && $query->offset > 0 ? (int)$query->offset : 0,
327
            $query->limit !== null && $query->limit >= 1 ? (int)$query->limit : null,
328
            $query->sortClauses
329
        );
330
331
        $trashItems = $this->buildDomainTrashItems($spiTrashItems);
332
        $searchResult = new SearchResult();
333
        $searchResult->totalCount = $searchResult->count = count($trashItems);
334
        $searchResult->items = $trashItems;
335
336
        return $searchResult;
337
    }
338
339
    protected function buildDomainTrashItems(array $spiTrashItems): array
340
    {
341
        $trashItems = array();
342
        // TODO: load content in bulk once API allows for it
343
        foreach ($spiTrashItems as $spiTrashItem) {
344
            try {
345
                $trashItems[] = $this->buildDomainTrashItemObject(
346
                    $spiTrashItem,
347
                    $this->repository->getContentService()->loadContent($spiTrashItem->contentId)
348
                );
349
            } catch (APIUnauthorizedException $e) {
350
                // Do nothing, thus exclude items the current user doesn't have read access to.
351
            }
352
        }
353
354
        return $trashItems;
355
    }
356
357
    protected function buildDomainTrashItemObject(Trashed $spiTrashItem, Content $content): APITrashItem
358
    {
359
        return new TrashItem(
360
            array(
361
                'content' => $content,
362
                'contentInfo' => $content->contentInfo,
363
                'id' => $spiTrashItem->id,
364
                'priority' => $spiTrashItem->priority,
365
                'hidden' => $spiTrashItem->hidden,
366
                'invisible' => $spiTrashItem->invisible,
367
                'remoteId' => $spiTrashItem->remoteId,
368
                'parentLocationId' => $spiTrashItem->parentId,
369
                'pathString' => $spiTrashItem->pathString,
370
                'depth' => $spiTrashItem->depth,
371
                'sortField' => $spiTrashItem->sortField,
372
                'sortOrder' => $spiTrashItem->sortOrder,
373
                'trashed' => isset($spiTrashItem->trashed) ? new DateTime('@' . $spiTrashItem->trashed) : new DateTime('@0'),
374
            )
375
        );
376
    }
377
378
    /**
379
     * @param int $timestamp
380
     *
381
     * @return \DateTime
382
     */
383
    protected function getDateTime($timestamp)
384
    {
385
        $dateTime = new DateTime();
386
        $dateTime->setTimestamp($timestamp);
387
388
        return $dateTime;
389
    }
390
391
    /**
392
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
393
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
394
     *
395
     * @return bool
396
     *
397
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
398
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
399
     */
400
    private function userHasPermissionsToRemove(ContentInfo $contentInfo, Location $location)
401
    {
402
        if (!$this->repository->canUser('content', 'remove', $contentInfo, [$location])) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Check if user has access to a given action on a given value object. Indicates if the current user is allowed to perform an action given by the function on the given
objects.

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

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

Loading history...
403
            return false;
404
        }
405
        $contentRemoveCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'remove');
406
        if (!$contentRemoveCriterion instanceof Criterion) {
407
            return (bool)$contentRemoveCriterion;
408
        }
409
        $query = new Query(
410
            array(
411
                'limit' => 0,
412
                'filter' => new CriterionLogicalAnd(
413
                    array(
414
                        new CriterionSubtree($location->pathString),
415
                        new CriterionLogicalNot($contentRemoveCriterion),
416
                    )
417
                ),
418
            )
419
        );
420
        $result = $this->repository->getSearchService()->findContent($query, array(), false);
421
422
        return $result->totalCount == 0;
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $result->totalCount of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
423
    }
424
}
425