Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
248 / 248
100.00% covered (success)
100.00%
35 / 35
CRAP
100.00% covered (success)
100.00%
1 / 1
Acl
100.00% covered (success)
100.00%
248 / 248
100.00% covered (success)
100.00%
35 / 35
169
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 getRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addRole
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 addRoles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getResource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasResource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addResource
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getResources
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addResources
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setStrict
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isStrict
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allow
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 removeAllowRule
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
12
 deny
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 removeDenyRule
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
12
 isAllowed
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
20
 isAllowedMany
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 isAllowedManyStrict
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isDenied
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
16
 isDeniedMany
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 isDeniedManyStrict
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createAssertion
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 deleteAssertion
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasAssertionKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getAssertionKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 addPolicy
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 hasPolicies
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 evaluatePolicies
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
19
 evaluatePolicy
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 verifyRole
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 verifyResource
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 generateAssertionKey
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 traverseChildren
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Acl;
15
16use Pop\Acl\Assertion\AssertionInterface;
17
18/**
19 * ACL class
20 *
21 * @category   Pop
22 * @package    Pop\Acl
23 * @author     Nick Sagona, III <dev@nolainteractive.com>
24 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
25 * @license    http://www.popphp.org/license     New BSD License
26 * @version    3.4.0
27 */
28class Acl
29{
30
31    /**
32     * Array of roles
33     * @var array
34     */
35    protected $roles = [];
36
37    /**
38     * Array of resources
39     * @var array
40     */
41    protected $resources = [];
42
43    /**
44     * Array of allowed roles, resources and permissions
45     * @var array
46     */
47    protected $allowed = [];
48
49    /**
50     * Array of denied roles, resources and permissions
51     * @var array
52     */
53    protected $denied = [];
54
55    /**
56     * Array of assertions
57     * @var array
58     */
59    protected $assertions = [
60        'allowed' => [],
61        'denied'  => []
62    ];
63
64    /**
65     * Array of policies
66     * @var array
67     */
68    protected $policies = [];
69
70    /**
71     * Strict flag
72     * @var boolean
73     */
74    protected $strict = false;
75
76    /**
77     * Constructor
78     *
79     * Instantiate the ACL object
80     */
81    public function __construct()
82    {
83        $args = func_get_args();
84
85        foreach ($args as $arg) {
86            if (is_array($arg)) {
87                foreach ($arg as $a) {
88                    if ($a instanceof AclRole) {
89                        $this->addRole($a);
90                    } else if ($a instanceof AclResource) {
91                        $this->addResource($a);
92                    }
93                }
94            } else if ($arg instanceof AclRole) {
95                $this->addRole($arg);
96            } else if ($arg instanceof AclResource) {
97                $this->addResource($arg);
98            }
99        }
100    }
101
102    /**
103     * Get a role
104     *
105     * @param  string $role
106     * @return AclRole
107     */
108    public function getRole($role)
109    {
110        return (isset($this->roles[$role])) ? $this->roles[$role] : null;
111    }
112
113    /**
114     * Get roles
115     *
116     * @return array
117     */
118    public function getRoles()
119    {
120        return $this->roles;
121    }
122
123    /**
124     * See if a role has been added
125     *
126     * @param  string $role
127     * @return boolean
128     */
129    public function hasRole($role)
130    {
131        return (isset($this->roles[$role]));
132    }
133
134    /**
135     * Add a role
136     *
137     * @param  AclRole $role
138     * @return Acl
139     */
140    public function addRole(AclRole $role)
141    {
142        if (!isset($this->roles[$role->getName()])) {
143            $this->roles[$role->getName()] = $role;
144
145            // Traverse up if role has parents
146            while ($role->hasParent()) {
147                $role = $role->getParent();
148                $this->roles[$role->getName()] = $role;
149            }
150
151            // Traverse down if the role has children
152            if ($role->hasChildren()) {
153                $this->traverseChildren($role->getChildren());
154            }
155        }
156        return $this;
157    }
158
159    /**
160     * Add roles
161     *
162     * @param  array $roles
163     * @return Acl
164     */
165    public function addRoles(array $roles)
166    {
167        foreach ($roles as $role) {
168            $this->addRole($role);
169        }
170
171        return $this;
172    }
173
174    /**
175     * Get a resource
176     *
177     * @param  string $resource
178     * @return AclResource
179     */
180    public function getResource($resource)
181    {
182        return (isset($this->resources[$resource])) ? $this->resources[$resource] : null;
183    }
184
185    /**
186     * See if a resource has been added
187     *
188     * @param  string $resource
189     * @return boolean
190     */
191    public function hasResource($resource)
192    {
193        return (isset($this->resources[$resource]));
194    }
195
196    /**
197     * Add a resource
198     *
199     * @param AclResource $resource
200     * @return Acl
201     */
202    public function addResource(AclResource $resource)
203    {
204        $this->resources[$resource->getName()] = $resource;
205        return $this;
206    }
207
208    /**
209     * Get resources
210     *
211     * @return array
212     */
213    public function getResources()
214    {
215        return $this->resources;
216    }
217
218    /**
219     * Add resources
220     *
221     * @param  array $resources
222     * @throws Exception
223     * @return Acl
224     */
225    public function addResources(array $resources)
226    {
227        foreach ($resources as $resource) {
228            $this->addResource($resource);
229        }
230
231        return $this;
232    }
233
234    /**
235     * Set strict
236     *
237     * @param  boolean $strict
238     * @return Acl
239     */
240    public function setStrict($strict = true)
241    {
242        $this->strict = (bool)$strict;
243        return $this;
244    }
245
246    /**
247     * See if ACL object is set to strict
248     *
249     * @return boolean
250     */
251    public function isStrict()
252    {
253        return $this->strict;
254    }
255
256    /**
257     * Allow a user role permission to a resource or resources
258     *
259     * @param  mixed              $role
260     * @param  mixed              $resource
261     * @param  mixed              $permission
262     * @param  AssertionInterface $assertion
263     * @throws Exception
264     * @return Acl
265     */
266    public function allow($role, $resource = null, $permission = null, AssertionInterface $assertion = null)
267    {
268        if ($this->verifyRole($role)) {
269            $role = $this->roles[(string)$role];
270
271            if (!isset($this->allowed[(string)$role])) {
272                $this->allowed[(string)$role] = [];
273            }
274
275            if ((null !== $resource) && ($this->verifyResource($resource))) {
276                $resource = $this->resources[(string)$resource];
277
278                if (!isset($this->allowed[(string)$role][(string)$resource])) {
279                    $this->allowed[(string)$role][(string)$resource] = [];
280                }
281                if (null !== $permission) {
282                    $this->allowed[(string)$role][(string)$resource][] = $permission;
283                }
284            }
285
286            // If an assertion has been passed
287            if (null !== $assertion) {
288                $this->createAssertion($assertion, 'allowed', $role, $resource, $permission);
289            }
290        }
291
292        return $this;
293    }
294
295    /**
296     * Remove an allow rule
297     *
298     * @param  mixed $role
299     * @param  mixed $resource
300     * @param  mixed $permission
301     * @throws Exception
302     * @return Acl
303     */
304    public function removeAllowRule($role, $resource = null, $permission = null)
305    {
306        if (($this->verifyRole($role)) && isset($this->allowed[(string)$role])) {
307            // If only role passed
308            if ((null === $resource) && (null === $permission)) {
309                unset($this->allowed[(string)$role]);
310            // If role & resource passed
311            } else if ((null !== $resource) && (null === $permission) && ($this->verifyResource($resource)) &&
312                isset($this->allowed[(string)$role][(string)$resource])) {
313                unset($this->allowed[(string)$role][(string)$resource]);
314            // If role, resource & permission passed
315            } else {
316                if (($this->verifyResource($resource)) && isset($this->allowed[(string)$role][(string)$resource]) &&
317                    in_array($permission, $this->allowed[(string)$role][(string)$resource])) {
318                    $key = array_search($permission, $this->allowed[(string)$role][(string)$resource]);
319                    unset($this->allowed[(string)$role][(string)$resource][$key]);
320                }
321            }
322
323            $this->deleteAssertion('allowed', $role, $resource, $permission);
324        }
325
326        return $this;
327    }
328
329    /**
330     * Deny a user role permission to a resource or resources
331     *
332     * @param  mixed              $role
333     * @param  mixed              $resource
334     * @param  mixed              $permission
335     * @param  AssertionInterface $assertion
336     * @throws Exception
337     * @return Acl
338     */
339    public function deny($role, $resource = null, $permission = null, AssertionInterface $assertion = null)
340    {
341        if ($this->verifyRole($role)) {
342            $role = $this->roles[(string)$role];
343
344            if (!isset($this->denied[(string)$role])) {
345                $this->denied[(string)$role] = [];
346            }
347
348            if ((null !== $resource) && ($this->verifyResource($resource))) {
349                $resource = $this->resources[(string)$resource];
350
351                if (!isset($this->denied[(string)$role][(string)$resource])) {
352                    $this->denied[(string)$role][(string)$resource] = [];
353                }
354                if (null !== $permission) {
355                    $this->denied[(string)$role][(string)$resource][] = $permission;
356                }
357            }
358
359            // If an assertion has been passed
360            if (null !== $assertion) {
361                $this->createAssertion($assertion, 'denied', $role, $resource, $permission);
362            }
363        }
364
365        return $this;
366    }
367
368    /**
369     * Remove a deny rule
370     *
371     * @param  mixed $role
372     * @param  mixed $resource
373     * @param  mixed $permission
374     * @throws Exception
375     * @return Acl
376     */
377    public function removeDenyRule($role, $resource = null, $permission = null)
378    {
379        if (($this->verifyRole($role)) && isset($this->denied[(string)$role])) {
380            // If only role passed
381            if ((null === $resource) && (null === $permission)) {
382                unset($this->denied[(string)$role]);
383            // If role & resource passed
384            } else if ((null !== $resource) && (null === $permission) && ($this->verifyResource($resource)) &&
385                isset($this->denied[(string)$role][(string)$resource])) {
386                unset($this->denied[(string)$role][(string)$resource]);
387            // If role, resource & permission passed
388            } else {
389                if (($this->verifyResource($resource)) && isset($this->denied[(string)$role][(string)$resource]) &&
390                    in_array($permission, $this->denied[(string)$role][(string)$resource])) {
391                    $key = array_search($permission, $this->denied[(string)$role][(string)$resource]);
392                    unset($this->denied[(string)$role][(string)$resource][$key]);
393                }
394            }
395
396            $this->deleteAssertion('denied', $role, $resource, $permission);
397        }
398
399        return $this;
400    }
401
402    /**
403     * Determine if the user is allowed
404     *
405     * @param  mixed $role
406     * @param  mixed $resource
407     * @param  mixed $permission
408     * @return boolean
409     */
410    public function isAllowed($role, $resource = null, $permission = null)
411    {
412        $result = false;
413
414        if ($this->verifyRole($role)) {
415            if (null !== $resource) {
416                $this->verifyResource($resource);
417            }
418
419            // If is not denied
420            if (!$this->isDenied($role, $resource, $permission)) {
421                // If not strict, pass
422                if (!$this->strict) {
423                    $result = true;
424                // If strict, check for explicit allow rule
425                } else {
426                    $roleToCheck = $this->roles[(string)$role];
427                    while (null !== $roleToCheck) {
428                        if (isset($this->allowed[(string)$roleToCheck])) {
429                            // No explicit resources or permissions
430                            if (count($this->allowed[(string)$roleToCheck]) == 0) {
431                                $result = true;
432                                // Resource set, but no explicit permissions
433                            } else if ((null !== $resource) && isset($this->allowed[(string)$roleToCheck][(string)$resource]) &&
434                                (count($this->allowed[(string)$roleToCheck][(string)$resource]) == 0)) {
435                                $result = true;
436                                // Else, has resource and permissions set
437                            } else if ((null !== $resource) && (null !== $permission) &&
438                                isset($this->allowed[(string)$roleToCheck][(string)$resource]) &&
439                                (count($this->allowed[(string)$roleToCheck][(string)$resource]) > 0)) {
440                                $permissions        = (!is_array($permission)) ? [$permission] : $permission;
441                                $allowedPermissions = array_intersect(
442                                    $permissions, $this->allowed[(string)$roleToCheck][(string)$resource]
443                                );
444
445                                $result = (count($allowedPermissions) == count($permissions));
446                            }
447                        }
448                        $roleToCheck = $roleToCheck->getParent();
449                    }
450                }
451            }
452        }
453
454        // Check for assertions
455        if (($result) && ($this->hasAssertionKey('allowed', $role, $resource, $permission))) {
456            $assertionKey      = $this->getAssertionKey('allowed', $role, $resource, $permission);
457            $assertionRole     = $this->roles[(string)$role];
458            $assertionResource = (null !== $resource) ?  $this->resources[(string)$resource] : null;
459            $result            =
460                $this->assertions['allowed'][$assertionKey]->assert($this, $assertionRole, $assertionResource, $permission);
461        }
462
463        // Check for policies
464        if ($this->hasPolicies()) {
465            $result = $this->evaluatePolicies($role, $resource, $permission);
466        }
467
468        return $result;
469    }
470
471    /**
472     * Determine if a user that is assigned many roles is allowed
473     * If one of the roles is allowed, then the user will be allowed (return true)
474     *
475     * @param  array $roles
476     * @param  mixed $resource
477     * @param  mixed $permission
478     * @throws Exception
479     * @return boolean
480     */
481    public function isAllowedMany(array $roles, $resource = null, $permission = null)
482    {
483        if ($this->strict) {
484            $result = true;
485            foreach ($roles as $role) {
486                if (!$this->isAllowed($role, $resource, $permission)) {
487                    $result = false;
488                    break;
489                }
490            }
491        } else {
492            $result = false;
493            foreach ($roles as $role) {
494                if ($this->isAllowed($role, $resource, $permission)) {
495                    $result = true;
496                    break;
497                }
498            }
499        }
500
501        return $result;
502    }
503
504    /**
505     * Determine if a user that is assigned many roles is allowed
506     * All of the roles must be allowed to allow the user (return true)
507     *
508     * @param  array $roles
509     * @param  mixed $resource
510     * @param  mixed $permission
511     * @return boolean
512     */
513    public function isAllowedManyStrict(array $roles, $resource = null, $permission = null)
514    {
515        $this->strict = true;
516        return $this->isAllowedMany($roles, $resource, $permission);
517    }
518
519    /**
520     * Determine if the user is denied
521     *
522     * @param  mixed $role
523     * @param  mixed $resource
524     * @param  mixed $permission
525     * @throws Exception
526     * @return boolean
527     */
528    public function isDenied($role, $resource = null, $permission = null)
529    {
530        $result = false;
531
532        if ($this->verifyRole($role)) {
533            if (null !== $resource) {
534                $this->verifyResource($resource);
535            }
536
537            // Check if the user, resource and/or permission is denied
538            $roleToCheck = $this->roles[(string)$role];
539            while (null !== $roleToCheck) {
540                if (isset($this->denied[(string)$roleToCheck])) {
541                    if (count($this->denied[(string)$roleToCheck]) > 0) {
542                        if ((null !== $resource) && array_key_exists((string)$resource, $this->denied[(string)$roleToCheck])) {
543                            if (count($this->denied[(string)$roleToCheck][(string)$resource]) > 0) {
544                                if (null !== $permission) {
545                                    $permissions = (!is_array($permission)) ? [$permission] : $permission;
546                                    foreach ($permissions as $p) {
547                                        if (in_array($p, $this->denied[(string)$roleToCheck][(string)$resource])) {
548                                            $result = true;
549                                        }
550                                    }
551                                }
552                            } else {
553                                $result = true;
554                            }
555                        }
556                    } else {
557                        $result = true;
558                    }
559                }
560                $roleToCheck = $roleToCheck->getParent();
561            }
562        }
563
564        // Check for assertions
565        if ($this->hasAssertionKey('denied', $role, $resource, $permission)) {
566            $assertionKey      = $this->getAssertionKey('denied', $role, $resource, $permission);
567            $assertionRole     = $this->roles[(string)$role];
568            $assertionResource = (null !== $resource) ?  $this->resources[(string)$resource] : null;
569            $result            =
570                $this->assertions['denied'][$assertionKey]->assert($this, $assertionRole, $assertionResource, $permission);
571        }
572
573        // Check for policies
574        if ($this->hasPolicies()) {
575            $result = (!$this->evaluatePolicies($role, $resource, $permission));
576        }
577
578        return $result;
579    }
580
581    /**
582     * Determine if a user that is assigned many roles is denied
583     * If one of the roles is denied, then the user will be denied (return true)
584     *
585     * @param  array $roles
586     * @param  mixed $resource
587     * @param  mixed $permission
588     * @throws Exception
589     * @return boolean
590     */
591    public function isDeniedMany(array $roles, $resource = null, $permission = null)
592    {
593        if ($this->strict) {
594            $result = true;
595            foreach ($roles as $role) {
596                if (!$this->isDenied($role, $resource, $permission)) {
597                    $result = false;
598                    break;
599                }
600            }
601        } else {
602            $result = false;
603            foreach ($roles as $role) {
604                if ($this->isDenied($role, $resource, $permission)) {
605                    $result = true;
606                    break;
607                }
608            }
609        }
610
611        return $result;
612    }
613
614    /**
615     * Determine if a user that is assigned many roles is denied
616     * All of the roles must be denied to deny the user (return true)
617     *
618     * @param  array $roles
619     * @param  mixed $resource
620     * @param  mixed $permission
621     * @throws Exception
622     * @return boolean
623     */
624    public function isDeniedManyStrict(array $roles, $resource = null, $permission = null)
625    {
626        $this->strict = true;
627        return $this->isDeniedMany($roles, $resource, $permission);
628    }
629
630    /**
631     * Create assertion
632     *
633     * @param  AssertionInterface $assertion
634     * @param  string             $type
635     * @param  mixed              $role
636     * @param  mixed              $resource
637     * @param  string             $permission
638     * @throws \InvalidArgumentException
639     * @return void
640     */
641    public function createAssertion(AssertionInterface $assertion, $type, $role, $resource = null, $permission = null)
642    {
643        $key = $this->generateAssertionKey($role, $resource, $permission);
644
645        if (($type != 'allowed') && ($type != 'denied')) {
646            throw new \InvalidArgumentException("Error: The assertion type must be either 'allowed' or 'denied'.");
647        }
648        $this->assertions[$type][$key] = $assertion;
649    }
650
651    /**
652     * Delete assertion
653     *
654     * @param  string $type
655     * @param  mixed  $role
656     * @param  mixed  $resource
657     * @param  string $permission
658     * @return void
659     */
660    public function deleteAssertion($type, $role, $resource = null, $permission = null)
661    {
662        $key = $this->generateAssertionKey($role, $resource, $permission);
663
664        if (isset($this->assertions[$type][$key])) {
665            unset($this->assertions[$type][$key]);
666        }
667    }
668
669    /**
670     * Has assertion key
671     *
672     * @param  string $type
673     * @param  mixed  $role
674     * @param  mixed  $resource
675     * @param  string $permission
676     * @throws \InvalidArgumentException
677     * @return boolean
678     */
679    public function hasAssertionKey($type, $role, $resource = null, $permission = null)
680    {
681        $key = $this->generateAssertionKey($role, $resource, $permission);
682
683        if (($type != 'allowed') && ($type != 'denied')) {
684            throw new \InvalidArgumentException("Error: The assertion type must be either 'allowed' or 'denied'.");
685        }
686
687        return (isset($this->assertions[$type][$key]));
688    }
689
690    /**
691     * Get assertion key
692     *
693     * @param  string $type
694     * @param  mixed  $role
695     * @param  mixed  $resource
696     * @param  string $permission
697     * @throws \InvalidArgumentException
698     * @return string
699     */
700    public function getAssertionKey($type, $role, $resource = null, $permission = null)
701    {
702        $key = $this->generateAssertionKey($role, $resource, $permission);
703
704        if (($type != 'allowed') && ($type != 'denied')) {
705            throw new \InvalidArgumentException("Error: The assertion type must be either 'allowed' or 'denied'.");
706        }
707
708        return (isset($this->assertions[$type][$key])) ? $key : null;
709    }
710
711    /**
712     * Add policy
713     *
714     * @param  string $method
715     * @param  mixed  $role
716     * @param  mixed  $resource
717     * @return Acl
718     */
719    public function addPolicy($method, $role, $resource = null)
720    {
721        $this->policies[] = [
722            'method'   => $method,
723            'role'     => $role,
724            'resource' => $resource
725        ];
726
727        return $this;
728    }
729
730    /**
731     * Has policies
732     *
733     * @return boolean
734     */
735    public function hasPolicies()
736    {
737        return (count($this->policies) > 0);
738    }
739
740    /**
741     * Evaluate policies
742     *
743     * @param  mixed $role
744     * @param  mixed $resource
745     * @param  mixed $permission
746     * @return boolean|null
747     */
748    public function evaluatePolicies($role = null, $resource = null, $permission = null)
749    {
750        $result = null;
751
752        if ((null === $role) && (null === $resource) && (null === $permission)) {
753            foreach ($this->policies as $policy) {
754                $result = $this->evaluatePolicy($policy['method'], $policy['role'], $policy['resource']);
755                if ($result === false) {
756                    return false;
757                }
758            }
759        } else {
760            $policyRole     = null;
761            $policyResource = null;
762            $policyMethod   = (null !== $permission) ? $permission : null;
763
764            if (null !== $role) {
765                $this->verifyRole($role);
766                $policyRole = ($role instanceof AclRole) ? $role->getName() : $role;
767            }
768            if (null !== $resource) {
769                $this->verifyResource($resource);
770                $policyResource = ($resource instanceof AclResource) ? $resource->getName() : $resource;
771            }
772
773            foreach ($this->policies as $policy) {
774                if (((null === $policyRole) || ($policyRole == $policy['role'])) &&
775                    ((null === $policyResource) || ($policyResource == $policy['resource'])) &&
776                    ((null === $policyMethod) || ($policyMethod == $policy['method']))) {
777                    $result = $this->evaluatePolicy($policy['method'], $policy['role'], $policy['resource']);
778                    if ($result === false) {
779                        return false;
780                    }
781                }
782            }
783        }
784
785        return $result;
786    }
787
788    /**
789     * Evaluate policy
790     *
791     * @param  string $method
792     * @param  mixed  $role
793     * @param  mixed  $resource
794     * @throws Exception
795     * @return boolean
796     */
797    public function evaluatePolicy($method, $role, $resource = null)
798    {
799        if (is_string($role) && ($this->verifyRole($role))) {
800            $role = $this->roles[(string)$role];
801        }
802
803        if (!in_array('Pop\Acl\Policy\PolicyTrait', class_uses($role))) {
804            throw new Exception('Error: The role must use Pop\Acl\Policy\PolicyTrait.');
805        }
806
807        if (null !== $resource) {
808            $this->verifyResource($resource);
809            $resource = $this->resources[(string)$resource];
810        }
811
812        return $role->can($method, $resource);
813    }
814
815    /**
816     * Verify role
817     *
818     * @param  mixed $role
819     * @throws Exception
820     * @return boolean
821     */
822    protected function verifyRole($role)
823    {
824        if (!is_string($role) && !($role instanceof AclRole)) {
825            throw new \InvalidArgumentException('Error: The role must be a string or an instance of Role.');
826        }
827        if (!isset($this->roles[(string)$role])) {
828            throw new Exception("Error: The role '" . (string)$role . "' has not been added.");
829        }
830
831        return true;
832    }
833
834    /**
835     * Verify resource
836     *
837     * @param  mixed $resource
838     * @throws Exception
839     * @return boolean
840     */
841    protected function verifyResource($resource)
842    {
843        if (!is_string($resource) && !($resource instanceof AclResource)) {
844            throw new \InvalidArgumentException('Error: The resource must be a string or an instance of Resource.');
845        }
846        if (!isset($this->resources[(string)$resource])) {
847            throw new Exception("Error: The resource '" . (string)$resource . "' has not been added.");
848        }
849
850        return true;
851    }
852
853    /**
854     * Generate assertion key
855     *
856     * @param  mixed  $role
857     * @param  mixed  $resource
858     * @param  string $permission
859     * @return string
860     */
861    protected function generateAssertionKey($role, $resource = null, $permission = null)
862    {
863        $key = (string)$role;
864
865        if (null !== $resource) {
866            $key .= '-' . (string)$resource;
867        }
868        if (null !== $permission) {
869            $key .= '-' . (string)$permission;
870        }
871
872        return $key;
873    }
874
875    /**
876     * Traverse child roles to add them to the ACL object
877     *
878     * @param  array $roles
879     * @return void
880     */
881    protected function traverseChildren(array $roles)
882    {
883        foreach ($roles as $role) {
884            $this->addRole($role);
885            if ($role->hasChildren()) {
886                $this->traverseChildren($role->getChildren());
887            }
888        }
889    }
890
891}