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