Completed
Push — develop ( 88d3fd...d2080b )
by
unknown
19:15 queued 10:13
created

Permissions::getResources()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
/**
3
 * YAWIK
4
 *
5
 * @filesource
6
 * @copyright (c) 2013 - 2016 Cross Solution (http://cross-solution.de)
7
 * @license   MIT
8
 */
9
10
/** Permissions.php */
11
namespace Core\Entity;
12
13
use Doctrine\Common\Collections\Collection;
14
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
15
use Auth\Entity\UserInterface;
16
use Core\Entity\Collection\ArrayCollection;
17
18
/**
19
 * Manages permissions for an entity.
20
 *
21
 *
22
 * @method boolean isAllGranted($userOrId)    shortcut for isGranted($userOrId, self::PERMISSION_ALL)
23
 * @method boolean isNoneGranted($userOrId)   shortcut for isGranted($userOrId, self::PERMISSION_NONE)
24
 * @method boolean isChangeGranted($userOrId) shortcut for isGranted($userOrId, self::PERMISSION_CHANGE)
25
 * @method boolean isViewGranted($userOrId)   shortcut for isGranted($userOrId, self::PERMISSION_VIEW)
26
 * @method $this grantAll($resource)          shortcut for grant($resource, self::PERMISSION_ALL)
27
 * @method $this grantNone($resource)         shortcut for grant($resource, self::PERMISSION_NONE)
28
 * @method $this grantChange($resource)       shortcut for grant($resource, self::PERMISSION_CHANGE)
29
 * @method $this grantView($resource)         shortcut for grant($resource, self::PERMISSION_VIEW)
30
 * @method $this revokeAll($resource)         shortcut for grant($resource, self::PERMISSION_ALL)
31
 * @method $this revokeNone($resource)        shortcut for grant($resource, self::PERMISSION_NONE)
32
 * @method $this revokeChange($resource)      shortcut for grant($resource, self::PERMISSION_CHANGE)
33
 * @method $this revokeView($resource)        shortcut for grant($resource, self::PERMISSION_VIEW)
34
 *
35
 * @ODM\EmbeddedDocument
36
 *
37
 * @author Mathias Gelhausen <[email protected]>
38
 */
39
class Permissions implements PermissionsInterface
40
{
41
    /**
42
     * The type of this Permissions
43
     *
44
     * default is the Fully qualified class name.
45
     *
46
     * @ODM\Field(type="string")
47
     * @var string
48
     * @since 0,18
49
     */
50
    protected $type;
51
52
    /**
53
     * Ids of users, which have view access.
54
     *
55
     * @var array
56
     * @ODM\Collection
57
     * @ODM\Index
58
     */
59
    protected $view = array();
60
61
    /**
62
     * Ids of users, which have change access.
63
     *
64
     * @var array
65
     * @ODM\Collection
66
     * @ODM\Index
67
     */
68
    protected $change = array();
69
    
70
    /**
71
     * Specification of assigned resources.
72
     *
73
     * As of 0.18, the format is:
74
     * <pre>
75
     * array(
76
     *  resourceId => array(
77
     *    permission => array(userId,...),
78
     *    ...
79
     *  ),
80
     *  ...
81
     * );
82
     * </pre>
83
     *
84
     * @var array
85
     * @ODM\Hash
86
     */
87
    protected $assigned = array();
88
    
89
    /**
90
     * Collection of all assigned resources.
91
     *
92
     * @var Collection
93
     * @ODM\ReferenceMany(discriminatorField="_resource")
94
     */
95
    protected $resources;
96
97
    /**
98
     * Flag, wether this permissions has changed or not.
99
     *
100
     * @var bool
101
     */
102
    protected $hasChanged = false;
103
104
    /**
105
     * Creates a Permissions instance.
106
     *
107
     * @param string|null $type The type identifier, defaults to FQCN.
108
     */
109
    public function __construct($type = null)
110
    {
111
        $this->type = $type ?: get_class($this);
112
    }
113
114
    /**
115
     * Clones resources in a new ArrayCollection.
116
     * Needed because PHP does not deep cloning objects.
117
     * (That means, references stay references pointing to the same
118
     *  object than the parent.)
119
     */
120
    public function __clone()
121
    {
122
        $resources = new ArrayCollection();
123
        if ($this->resources) {
124
            foreach ($this->resources as $r) {
125
                $resources->add($r);
126
            }
127
        }
128
        $this->resources = $resources;
129
    }
130
131
    /**
132
     * Provides magic methods.
133
     *
134
     * - is[View|Change|None|All]Granted($user)
135
     * - grant[View|Change|None|All]($user)
136
     * - revoke[View|Change|None|All($user)
137
     *
138
     * @param string $method
139
     * @param array $params
140
     *
141
     * @return self|bool
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|PermissionsInterface?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
142
     * @throws \InvalidArgumentException
143
     * @throws \BadMethodCallException
144
     */
145
    public function __call($method, $params)
146
    {
147
        if (1 != count($params)) {
148
            throw new \InvalidArgumentException('Missing required parameter.');
149
        }
150
151 View Code Duplication
        if (preg_match('~^is(View|Change|None|All)Granted$~', $method, $match)) {
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...
152
            $permission = constant('self::PERMISSION_' . strtoupper($match[1]));
153
            return $this->isGranted($params[0], $permission);
154
        }
155
        
156 View Code Duplication
        if (preg_match('~^grant(View|Change|None|All)$~', $method, $match)) {
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...
157
            $permission = constant('self::PERMISSION_' . strtoupper($match[1]));
158
            return $this->grant($params[0], $permission);
159
        }
160
        
161 View Code Duplication
        if (preg_match('~^revoke(View|Change|None|All)$~', $method, $match)) {
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...
162
            $permission = constant('self::PERMISSION_' . strtoupper($match[1]));
163
            return $this->revoke($params[0], $permission);
164
        }
165
        
166
        throw new \BadMethodCallException('Unknown method "' . $method . '"');
167
    }
168
169
    /**
170
     * Gets the permission type
171
     *
172
     * @return string
173
     * @since 0.24
174
     */
175
    public function getType()
176
    {
177
        return $this->type;
178
    }
179
180
    /**
181
     * Grants a permission to a user or resource.
182
     *
183
     * {@inheritDoc}
184
     *
185
     * @param bool $build Should the view and change arrays be rebuild?
186
     */
187
    public function grant($resource, $permission = null, $build = true)
188
    {
189
        if (is_array($resource)) {
190
            foreach ($resource as $r) {
191
                $this->grant($r, $permission, false);
192
            }
193
            if ($build) {
194
                $this->build();
195
            }
196
            return $this;
197
        }
198
199
        //new \Doctrine\ODM\MongoDB\
200
        true === $permission
201
        || (null === $permission && $resource instanceof PermissionsResourceInterface)
202
        || $this->checkPermission($permission);
203
        
204
        $resourceId = $this->getResourceId($resource);
205
        
206
        if (true === $permission) {
207
            $permission = $this->getFrom($resource);
208
        }
209
        
210
        if (self::PERMISSION_NONE == $permission) {
211
            if ($resource instanceof PermissionsResourceInterface) {
212
                $refs = $this->getResources();
213
                if ($refs->contains($resource)) {
214
                    $refs->removeElement($resource);
215
                }
216
            }
217
            unset($this->assigned[$resourceId]);
218
        } else {
219
            if ($resource instanceof PermissionsResourceInterface) {
220
                $spec = $resource->getPermissionsUserIds($this->type);
0 ignored issues
show
Unused Code introduced by
The call to PermissionsResourceInter...getPermissionsUserIds() has too many arguments starting with $this->type.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
221
                if (!is_array($spec) || !count($spec)) {
222
                    $spec = array();
223
                } elseif (is_numeric(key($spec))) {
224
                    $spec = array($permission => $spec);
225
                }
226
            } else {
227
                $spec = array($permission => $resource instanceof UserInterface ? array($resource->getId()) : array($resource));
228
            }
229
230
            $this->assigned[$resourceId] = $spec;
231
232
            if ($resource instanceof PermissionsResourceInterface) {
233
                try {
234
                    $refs = $this->getResources();
235
                    if (!$refs->contains($resource)) {
236
                        $refs->add($resource);
237
                    }
238
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
239
                };
240
            }
241
        }
242
        
243
        if ($build) {
244
            $this->build();
245
        }
246
        $this->hasChanged = true;
247
        return $this;
248
    }
249
250
    /**
251
     * Revokes a permission from a user or resource.
252
     *
253
     * {@inheritDoc}
254
     *
255
     * @param bool $build Should the view and change arrays be rebuild?
256
     *
257
     * @return $this|PermissionsInterface
258
     */
259
    public function revoke($resource, $permission = null, $build = true)
260
    {
261
        
262
        if (self::PERMISSION_NONE == $permission || !$this->isAssigned($resource)) {
263
            return $this;
264
        }
265
        
266
        if (self::PERMISSION_CHANGE == $permission) {
267
            return $this->grant($resource, self::PERMISSION_VIEW, $build);
268
        }
269
        
270
        return $this->grant($resource, self::PERMISSION_NONE, $build);
271
        
272
    }
273
    
274
    public function clear()
275
    {
276
        $this->view      = array();
277
        $this->change    = array();
278
        $this->assigned  = array();
279
        $this->resources = null;
280
        
281
        return $this;
282
    }
283
    
284
    public function inherit(PermissionsInterface $permissions, $build = true)
285
    {
286
        // Override permissions type temporarly to get the right permissions back
287
        // from resources which may be aware of the permissions type.
288
        // Maybe this must be controllable by an additional parameter, but for now
289
        // we make this default.
290
        $oldType = $this->type;
291
        $this->type = $permissions->getType();
292
293
        /* @var $permissions Permissions */
294
        $assigned  = $permissions->getAssigned();
295
        $resources = $permissions->getResources();
296
    
297
        /*
298
         * Grant resource references permissions.
299
         */
300
        foreach ($resources as $resource) {
301
            /* @var $resource PermissionsResourceInterface */
302
            $permission = $permissions->getFrom($resource);
303
304
            $this->grant($resource, $permission, false);
305
            unset($assigned[$resource->getPermissionsResourceId()]);
306
        }
307
        /*
308
         * Merge remaining user permissions (w/o resource references)
309
         */
310
        $this->assigned = array_merge($this->assigned, $assigned);
311
        if ($build) {
312
            $this->build();
313
        }
314
        $this->hasChanged= true;
315
316
        // restore orginial permissions type
317
        $this->type = $oldType;
318
319
        return $this;
320
    }
321
322
    /**
323
     * Builds the user id lists.
324
     *
325
     * This will make database queries faster and also the calls to {@link isGranted()} will be faster.
326
     *
327
     * @return self
328
     */
329
    public function build()
330
    {
331
        $view = $change = array();
332
        foreach ($this->assigned as $resourceId => $spec) {
333
            /* This is needed to convert old permissions to the new spec format
334
             * introduced in 0.18
335
             * TODO: Remove this line some versions later.
336
             */
337
            // @codeCoverageIgnoreStart
338
            if (isset($spec['permission'])) {
339
                $spec = array($spec['permission'] => $spec['users']);
340
                $this->assigned[$resourceId] = $spec;
341
            }
342
            // @codeCoverageIgnoreEnd
343
344
            foreach ($spec as $perm => $userIds) {
345
                if (self::PERMISSION_ALL == $perm || self::PERMISSION_CHANGE == $perm) {
346
                    $change = array_merge($change, $userIds);
347
                }
348
                $view = array_merge($view, $userIds);
349
            }
350
        }
351
        
352
        $this->change = array_unique($change);
353
        $this->view   = array_unique($view);
354
        return $this;
355
    }
356
    
357
    private function checkIsGranted($userId, $permission)
0 ignored issues
show
Coding Style introduced by
function checkIsGranted() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
358
    {
359
        if (!$userId) { return false; }
360
361
        if (self::PERMISSION_NONE == $permission) {
362
            return !in_array($userId, $this->view);
363
        }
364
        
365
        if (self::PERMISSION_ALL == $permission || self::PERMISSION_CHANGE == $permission) {
366
            return in_array($userId, $this->change);
367
        }
368
369
        // Now there's only PERMISSION_VIEW left to check.
370
        return in_array($userId, $this->view);
371
    }
372
373
    public function isGranted($userOrId, $permission)
374
    {
375
        if ($userOrId instanceOf UserInterface) {
376
            $id = $userOrId->getId();
377
            $role = $userOrId->getRole();
378
        } else {
379
            $id = (string) $userOrId;
380
            $role = null;
381
        }
382
383
        $this->checkPermission($permission);
384
385
        return $this->checkIsGranted($id, $permission)
386
               || ($this->isAssigned($role) && $this->checkIsGranted($role, $permission))
387
               || ($this->isAssigned('all') && $this->checkIsGranted('all', $permission));
388
    }
389
    
390
    public function isAssigned($resource)
391
    {
392
        $resourceId = $this->getResourceId($resource);
393
        return isset($this->assigned[$resourceId]);
394
    }
395
    
396
    public function hasChanged()
397
    {
398
        return $this->hasChanged;
399
    }
400
401
    /**
402
     * Gets the assigned specification.
403
     *
404
     * This is only needed when inheriting this permissions object into another.
405
     *
406
     * @return array
407
     */
408
    public function getAssigned()
409
    {
410
        return $this->assigned;
411
    }
412
413
    /**
414
     * Gets the resource collection.
415
     *
416
     * This is only needed when inheriting.
417
     *
418
     * @return Collection
419
     */
420
    public function getResources()
421
    {
422
        if (!$this->resources) {
423
            $this->resources = new ArrayCollection();
424
        }
425
        return $this->resources;
426
    }
427
428
429
    public function getFrom($resource)
430
    {
431
        $resourceId = $this->getResourceId($resource);
432
433
        if (!isset($this->assigned[$resourceId])) {
434
            return self::PERMISSION_NONE;
435
        }
436
437
        $spec = $this->assigned[$resourceId];
438
439
        return 1 == count($spec) ? key($spec) : null;
440
    }
441
    
442
443
    /**
444
     * Gets/Generates the resource id.
445
     *
446
     * @param string|UserInterface|PermissionsResourceInterface $resource
447
     *
448
     * @return string
449
     */
450
    protected function getResourceId($resource)
451
    {
452
        if ($resource instanceof PermissionsResourceInterface) {
453
            return $resource->getPermissionsResourceId();
454
        }
455
        
456
        if ($resource instanceof UserInterface) {
457
            return 'user:' . $resource->getId();
458
        }
459
        
460
        return 'user:' . $resource;
461
    }
462
463
    /**
464
     * Checks a valid permission.
465
     *
466
     * @param string $permission
467
     *
468
     * @throws \InvalidArgumentException
469
     */
470
    protected function checkPermission($permission)
471
    {
472
        $perms = array(
473
            self::PERMISSION_ALL,
474
            self::PERMISSION_CHANGE,
475
            self::PERMISSION_NONE,
476
            self::PERMISSION_VIEW,
477
        );
478
        if (!in_array($permission, $perms)) {
479
            throw new \InvalidArgumentException(
480
                'Invalid permission. Must be one of ' . implode(', ', $perms)
481
            );
482
        }
483
    }
484
}
485