Permissions   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Test Coverage

Coverage 98.62%

Importance

Changes 0
Metric Value
wmc 69
eloc 142
dl 0
loc 455
ccs 143
cts 145
cp 0.9862
rs 2.88
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A revoke() 0 11 4
A checkPermission() 0 11 2
A isGranted() 0 15 6
A getResources() 0 6 2
F grant() 0 61 20
A clear() 0 8 1
A inherit() 0 36 3
A hasChanged() 0 3 1
A __call() 0 22 5
A getResourceId() 0 11 3
A getType() 0 3 1
A checkIsGranted() 0 16 5
A getAssigned() 0 3 1
A getFrom() 0 11 3
A build() 0 26 6
A isAssigned() 0 4 1
A __construct() 0 3 2
A __clone() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like Permissions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Permissions, and based on these observations, apply Extract Interface, too.

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
 * @ODM\HasLifeCycleCallbacks
37
 *
38
 * @author Mathias Gelhausen <[email protected]>
39
 */
40
class Permissions implements PermissionsInterface
41
{
42
    /**
43
     * The type of this Permissions
44
     *
45
     * default is the Fully qualified class name.
46
     *
47
     * @ODM\Field(type="string")
48
     * @var string
49
     * @since 0,18
50
     */
51
    protected $type;
52
53
    /**
54
     * Ids of users, which have view access.
55
     *
56
     * @var array
57
     * @ODM\Field(type="collection")
58
     * @ODM\Index
59
     */
60
    protected $view = array();
61
62
    /**
63
     * Ids of users, which have change access.
64
     *
65
     * @var array
66
     * @ODM\Field(type="collection")
67
     * @ODM\Index
68
     */
69
    protected $change = array();
70
    
71
    /**
72
     * Specification of assigned resources.
73
     *
74
     * As of 0.18, the format is:
75
     * <pre>
76
     * array(
77
     *  resourceId => array(
78
     *    permission => array(userId,...),
79
     *    ...
80
     *  ),
81
     *  ...
82
     * );
83
     * </pre>
84
     *
85
     * @var array
86
     * @ODM\Field(type="hash")
87
     */
88
    protected $assigned = array();
89
    
90
    /**
91
     * Collection of all assigned resources.
92
     *
93
     * @var Collection
94
     * @ODM\ReferenceMany(discriminatorField="_resource")
95
     */
96
    protected $resources;
97
98
    /**
99
     * Flag, wether this permissions has changed or not.
100
     *
101
     * @var bool
102
     */
103
    protected $hasChanged = false;
104
105
    /**
106
     * Creates a Permissions instance.
107
     *
108
     * @param string|null $type The type identifier, defaults to FQCN.
109
     */
110 42
    public function __construct($type = null)
111
    {
112 42
        $this->type = $type ?: get_class($this);
113
    }
114
115
    /**
116
     * Clones resources in a new ArrayCollection.
117
     * Needed because PHP does not deep cloning objects.
118
     * (That means, references stay references pointing to the same
119
     *  object than the parent.)
120
     */
121 2
    public function __clone()
122
    {
123 2
        $resources = new ArrayCollection();
124 2
        if ($this->resources) {
125 1
            foreach ($this->resources as $r) {
126 1
                $resources->add($r);
127
            }
128
        }
129 2
        $this->resources = $resources;
130
    }
131
132
    /**
133
     * Provides magic methods.
134
     *
135
     * - is[View|Change|None|All]Granted($user)
136
     * - grant[View|Change|None|All]($user)
137
     * - revoke[View|Change|None|All($user)
138
     *
139
     * @param string $method
140
     * @param array $params
141
     *
142
     * @return self|bool
143
     * @throws \InvalidArgumentException
144
     * @throws \BadMethodCallException
145
     */
146 4
    public function __call($method, $params)
147
    {
148 4
        if (1 != count($params)) {
149 1
            throw new \InvalidArgumentException('Missing required parameter.');
150
        }
151
152 3
        if (preg_match('~^is(View|Change|None|All)Granted$~', $method, $match)) {
153 1
            $permission = constant('self::PERMISSION_' . strtoupper($match[1]));
154 1
            return $this->isGranted($params[0], $permission);
155
        }
156
        
157 3
        if (preg_match('~^grant(View|Change|None|All)$~', $method, $match)) {
158 2
            $permission = constant('self::PERMISSION_' . strtoupper($match[1]));
159 2
            return $this->grant($params[0], $permission);
160
        }
161
        
162 2
        if (preg_match('~^revoke(View|Change|None|All)$~', $method, $match)) {
163 1
            $permission = constant('self::PERMISSION_' . strtoupper($match[1]));
164 1
            return $this->revoke($params[0], $permission);
165
        }
166
        
167 1
        throw new \BadMethodCallException('Unknown method "' . $method . '"');
168
    }
169
170
    /**
171
     * Gets the permission type
172
     *
173
     * @return string
174
     * @since 0.24
175
     */
176 5
    public function getType()
177
    {
178 5
        return $this->type;
179
    }
180
181
    /**
182
     * Grants a permission to a user or resource.
183
     *
184
     * {@inheritDoc}
185
     *
186
     * @param bool $build Should the view and change arrays be rebuild?
187
     */
188 34
    public function grant($resource, $permission = null, $build = true)
189
    {
190 34
        if (is_array($resource)) {
191 1
            foreach ($resource as $r) {
192 1
                $this->grant($r, $permission, false);
193
            }
194 1
            if ($build) {
195 1
                $this->build();
196
            }
197 1
            return $this;
198
        }
199
200
        //new \Doctrine\ODM\MongoDB\
201 34
        true === $permission
202 34
        || (null === $permission && $resource instanceof PermissionsResourceInterface)
203 33
        || $this->checkPermission($permission);
204
        
205 33
        $resourceId = $this->getResourceId($resource);
206
        
207 33
        if (true === $permission) {
208 1
            $permission = $this->getFrom($resource);
209
        }
210
        
211 33
        if (self::PERMISSION_NONE == $permission) {
212 2
            if ($resource instanceof PermissionsResourceInterface) {
213 1
                $refs = $this->getResources();
214 1
                if ($refs->contains($resource)) {
215 1
                    $refs->removeElement($resource);
216
                }
217
            }
218 2
            unset($this->assigned[$resourceId]);
219
        } else {
220 33
            if ($resource instanceof PermissionsResourceInterface) {
221 7
                $spec = $resource->getPermissionsUserIds($this->type);
0 ignored issues
show
Unused Code introduced by
The call to Core\Entity\PermissionsR...getPermissionsUserIds() has too many arguments starting with $this->type. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

221
                /** @scrutinizer ignore-call */ 
222
                $spec = $resource->getPermissionsUserIds($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. Please note the @ignore annotation hint above.

Loading history...
222 7
                if (!is_array($spec) || !count($spec)) {
223 4
                    $spec = array();
224 4
                } elseif (is_numeric(key($spec))) {
225 7
                    $spec = array($permission => $spec);
226
                }
227
            } else {
228 28
                $spec = array($permission => $resource instanceof UserInterface ? array($resource->getId()) : array($resource));
229
            }
230
231 33
            $this->assigned[$resourceId] = $spec;
232
233 33
            if ($resource instanceof PermissionsResourceInterface) {
234
                try {
235 7
                    $refs = $this->getResources();
236 7
                    if (!$refs->contains($resource)) {
237 7
                        $refs->add($resource);
238
                    }
239
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
240
                };
241
            }
242
        }
243
        
244 33
        if ($build) {
245 31
            $this->build();
246
        }
247 33
        $this->hasChanged = true;
248 33
        return $this;
249
    }
250
251
    /**
252
     * Revokes a permission from a user or resource.
253
     *
254
     * {@inheritDoc}
255
     *
256
     * @param bool $build Should the view and change arrays be rebuild?
257
     *
258
     * @return $this|PermissionsInterface
259
     */
260 7
    public function revoke($resource, $permission = null, $build = true)
261
    {
262 7
        if (self::PERMISSION_NONE == $permission || !$this->isAssigned($resource)) {
263 2
            return $this;
264
        }
265
        
266 5
        if (self::PERMISSION_CHANGE == $permission) {
267 4
            return $this->grant($resource, self::PERMISSION_VIEW, $build);
268
        }
269
        
270 1
        return $this->grant($resource, self::PERMISSION_NONE, $build);
271
    }
272
    
273 1
    public function clear()
274
    {
275 1
        $this->view      = array();
276 1
        $this->change    = array();
277 1
        $this->assigned  = array();
278 1
        $this->resources = null;
279
        
280 1
        return $this;
281
    }
282
    
283 5
    public function inherit(PermissionsInterface $permissions, $build = true)
284
    {
285
        // Override permissions type temporarly to get the right permissions back
286
        // from resources which may be aware of the permissions type.
287
        // Maybe this must be controllable by an additional parameter, but for now
288
        // we make this default.
289 5
        $oldType = $this->type;
290 5
        $this->type = $permissions->getType();
0 ignored issues
show
Bug introduced by
The method getType() does not exist on Core\Entity\PermissionsInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Core\Entity\PermissionsInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

290
        /** @scrutinizer ignore-call */ 
291
        $this->type = $permissions->getType();
Loading history...
291
292
        /* @var $permissions Permissions */
293 5
        $assigned  = $permissions->getAssigned();
294 5
        $resources = $permissions->getResources();
295
    
296
        /*
297
         * Grant resource references permissions.
298
         */
299 5
        foreach ($resources as $resource) {
300
            /* @var $resource PermissionsResourceInterface */
301 1
            $permission = $permissions->getFrom($resource);
302
303 1
            $this->grant($resource, $permission, false);
304 1
            unset($assigned[$resource->getPermissionsResourceId()]);
305
        }
306
        /*
307
         * Merge remaining user permissions (w/o resource references)
308
         */
309 5
        $this->assigned = array_merge($this->assigned, $assigned);
310 5
        if ($build) {
311 5
            $this->build();
312
        }
313 5
        $this->hasChanged= true;
314
315
        // restore orginial permissions type
316 5
        $this->type = $oldType;
317
318 5
        return $this;
319
    }
320
321
    /**
322
     * Builds the user id lists.
323
     *
324
     * This will make database queries faster and also the calls to {@link isGranted()} will be faster.
325
     *
326
     * @return self
327
     */
328 34
    public function build()
329
    {
330 34
        $view = $change = array();
331 34
        foreach ($this->assigned as $resourceId => $spec) {
332
            /* This is needed to convert old permissions to the new spec format
333
             * introduced in 0.18
334
             * TODO: Remove this line some versions later.
335
             */
336
            // @codeCoverageIgnoreStart
337
            if (isset($spec['permission'])) {
338
                $spec = array($spec['permission'] => $spec['users']);
339
                $this->assigned[$resourceId] = $spec;
340
            }
341
            // @codeCoverageIgnoreEnd
342
343 33
            foreach ($spec as $perm => $userIds) {
344 30
                if (self::PERMISSION_ALL == $perm || self::PERMISSION_CHANGE == $perm) {
345 19
                    $change = array_merge($change, $userIds);
346
                }
347 30
                $view = array_merge($view, $userIds);
348
            }
349
        }
350
        
351 34
        $this->change = array_unique($change);
352 34
        $this->view   = array_unique($view);
353 34
        return $this;
354
    }
355
    
356 21
    private function checkIsGranted($userId, $permission)
357
    {
358 21
        if (!$userId) {
359
            return false;
360
        }
361
362 21
        if (self::PERMISSION_NONE == $permission) {
363 9
            return !in_array($userId, $this->view);
364
        }
365
        
366 21
        if (self::PERMISSION_ALL == $permission || self::PERMISSION_CHANGE == $permission) {
367 13
            return in_array($userId, $this->change);
368
        }
369
370
        // Now there's only PERMISSION_VIEW left to check.
371 20
        return in_array($userId, $this->view);
372
    }
373
374 21
    public function isGranted($userOrId, $permission)
375
    {
376 21
        if ($userOrId instanceof UserInterface) {
377 11
            $id = $userOrId->getId();
378 11
            $role = $userOrId->getRole();
379
        } else {
380 10
            $id = (string) $userOrId;
381 10
            $role = null;
382
        }
383
384 21
        $this->checkPermission($permission);
385
386 21
        return $this->checkIsGranted($id, $permission)
387 21
               || ($this->isAssigned($role) && $this->checkIsGranted($role, $permission))
388 21
               || ($this->isAssigned('all') && $this->checkIsGranted('all', $permission));
389
    }
390
    
391 22
    public function isAssigned($resource)
392
    {
393 22
        $resourceId = $this->getResourceId($resource);
394 22
        return isset($this->assigned[$resourceId]);
395
    }
396
    
397 1
    public function hasChanged()
398
    {
399 1
        return $this->hasChanged;
400
    }
401
402
    /**
403
     * Gets the assigned specification.
404
     *
405
     * This is only needed when inheriting this permissions object into another.
406
     *
407
     * @return array
408
     */
409 7
    public function getAssigned()
410
    {
411 7
        return $this->assigned;
412
    }
413
414
    /**
415
     * Gets the resource collection.
416
     *
417
     * This is only needed when inheriting.
418
     *
419
     * @internal
420
     *      The PrePersist hook is needed, because eventually
421
     *      this method is called during the onFlush event by
422
     *      an UpdateFilePermission-Listener. Generating
423
     *      an ArrayCollection in this state leads to a
424
     *      fatal error deep in Doctrine.
425
     *
426
     *      This PrePersist hook assures, that there is
427
     *      a prefilled ArrayCollection during the
428
     *      changeset computation.
429
     *
430
     * @ODM\PrePersist
431
     *
432
     * @return Collection
433
     */
434 12
    public function getResources()
435
    {
436 12
        if (!$this->resources) {
437 12
            $this->resources = new ArrayCollection();
438
        }
439 12
        return $this->resources;
440
    }
441
442
443 3
    public function getFrom($resource)
444
    {
445 3
        $resourceId = $this->getResourceId($resource);
446
447 3
        if (!isset($this->assigned[$resourceId])) {
448 1
            return self::PERMISSION_NONE;
449
        }
450
451 3
        $spec = $this->assigned[$resourceId];
452
453 3
        return 1 == count($spec) ? key($spec) : null;
454
    }
455
    
456
457
    /**
458
     * Gets/Generates the resource id.
459
     *
460
     * @param string|UserInterface|PermissionsResourceInterface $resource
461
     *
462
     * @return string
463
     */
464 35
    protected function getResourceId($resource)
465
    {
466 35
        if ($resource instanceof PermissionsResourceInterface) {
467 7
            return $resource->getPermissionsResourceId();
468
        }
469
        
470 30
        if ($resource instanceof UserInterface) {
471 11
            return 'user:' . $resource->getId();
472
        }
473
        
474 28
        return 'user:' . $resource;
475
    }
476
477
    /**
478
     * Checks a valid permission.
479
     *
480
     * @param string $permission
481
     *
482
     * @throws \InvalidArgumentException
483
     */
484 35
    protected function checkPermission($permission)
485
    {
486
        $perms = array(
487 35
            self::PERMISSION_ALL,
488 35
            self::PERMISSION_CHANGE,
489 35
            self::PERMISSION_NONE,
490 35
            self::PERMISSION_VIEW,
491
        );
492 35
        if (!in_array($permission, $perms)) {
493 1
            throw new \InvalidArgumentException(
494 1
                'Invalid permission. Must be one of ' . implode(', ', $perms)
495
            );
496
        }
497
    }
498
}
499