PluginProbe
Advanced Access Manager – Access Governance for WordPress / 6.9.26
Advanced Access Manager – Access Governance for WordPress v6.9.26
7.1.4 7.1.2 7.1.3 6.8.4 6.8.5 6.9.0 6.9.1 6.9.10 6.9.11 6.9.12 6.9.13 6.9.14 6.9.15 6.9.16 6.9.17 6.9.18 6.9.19 6.9.2 6.9.20 6.9.21 6.9.22 6.9.23 6.9.24 6.9.25 6.9.26 All 210 releases
advanced-access-manager / application / Core / Policy / Manager.php
Manager.php
766 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * ======================================================================
5 * LICENSE: This file is subject to the terms and conditions defined in *
6 * file 'license.txt', which is part of this source code package. *
7 * ======================================================================
8 */
9
10 /**
11 * AAM policy manager for a specific subject
12 *
13 * @since 6.9.25 https://github.com/aamplugin/advanced-access-manager/issues/353
14 * https://github.com/aamplugin/advanced-access-manager/issues/355
15 * @since 6.9.24 https://github.com/aamplugin/advanced-access-manager/issues/351
16 * @since 5.5.4 https://github.com/aamplugin/advanced-access-manager/issues/128
17 * @since 6.5.3 https://github.com/aamplugin/advanced-access-manager/issues/122
18 * https://github.com/aamplugin/advanced-access-manager/issues/124
19 * @since 6.4.0 Supporting Param's "Value" to be an array
20 * @since 6.3.1 Fixed bug where draft policies get applied to assignees
21 * @since 6.2.1 Added support for the POLICY_META token
22 * @since 6.2.0 Fetched the way access policies are fetched
23 * @since 6.1.0 Implemented `=>` operator. Improved inheritance mechanism
24 * @since 6.0.4 Potential bug fix with improperly merged Param option:* values
25 * @since 6.0.0 Initial implementation of the class
26 *
27 * @package AAM
28 * @version 6.9.25
29 */
30 class AAM_Core_Policy_Manager
31 {
32
33 /**
34 * Policy core object
35 *
36 * @var AAM_Core_Object_Policy
37 *
38 * @access protected
39 * @version 6.0.0
40 */
41 protected $object;
42
43 /**
44 * Parsed policy tree
45 *
46 * @var array
47 *
48 * @access protected
49 * @version 6.0.0
50 */
51 protected $tree = array(
52 'Statement' => array(),
53 'Param' => array()
54 );
55
56 /**
57 * Effect stemming map
58 *
59 * @var array
60 *
61 * @access private
62 * @version 6.9.24
63 */
64 private $_effects = array();
65
66 /**
67 * Constructor
68 *
69 * @param AAM_Core_Subject $subject
70 * @param boolean $skip_inheritance
71 *
72 * @access protected
73 *
74 * @since 6.1.0 Added new `$skip_inheritance` mandatory argument
75 * @since 6.0.0 Initial implementation of the method
76 *
77 * @return void
78 * @version 6.1.0
79 */
80 public function __construct(AAM_Core_Subject $subject, $skip_inheritance)
81 {
82 $this->object = $subject->getObject(
83 AAM_Core_Object_Policy::OBJECT_TYPE, null, $skip_inheritance
84 );
85
86 $this->_effects = apply_filters('aam_access_policy_effects_filter', array(
87 'allowed' => 'allow',
88 'denied' => 'deny',
89 ));
90 }
91
92 /**
93 * Parse all attached policies into the tree
94 *
95 * @return void
96 *
97 * @since 6.5.3 https://github.com/aamplugin/advanced-access-manager/issues/124
98 * @since 6.4.1 Changed the way updatePolicyTree is invoked
99 * @since 6.3.1 Fixed bug https://github.com/aamplugin/advanced-access-manager/issues/49
100 * @since 6.2.0 Changed the way access policies are fetched
101 * @since 6.0.0 Initial implementation of the method
102 *
103 * @access public
104 * @version 6.5.3
105 */
106 public function initialize()
107 {
108 // Get the list of all policies that are attached to the subject
109 $ids = array_filter($this->object->getOption(), function ($attached) {
110 return !empty($attached);
111 });
112
113 // If there is at least one policy attached and it is published, then
114 // parse into the tree
115 if (count($ids)) {
116 $policies = $this->fetchPolicies(array(
117 'post_status' => array('publish'),
118 'include' => array_keys($ids)
119 ));
120
121 foreach ($policies as $policy) {
122 $this->updatePolicyTree($this->parsePolicy($policy));
123 }
124
125 $this->_cleanupTree($this->tree['Statement']);
126 }
127 }
128
129 /**
130 * Fetch public policies by IDs
131 *
132 * @param array $ids
133 *
134 * @return array
135 *
136 * @since 6.2.0 Changed the way access policies are fetched to support multisite
137 * network setup
138 * @since 6.0.0 Initial implementation of the method
139 *
140 * @access protected
141 * @version 6.2.0
142 */
143 public function fetchPolicies($args = array())
144 {
145 do_action('aam_pre_policy_fetch_action');
146
147 $posts = get_posts(wp_parse_args($args, array(
148 'post_status' => array('publish', 'draft', 'pending'),
149 'suppress_filters' => true,
150 'post_type' => AAM_Service_AccessPolicy::POLICY_CPT,
151 'nopaging' => true
152 )));
153
154 do_action('aam_post_policy_fetch_action');
155
156 return $posts;
157 }
158
159 /**
160 * Evaluate unknown method
161 *
162 * Tries to process methods like isAllowed or isDeniedTo. This method recognizes
163 * and executes the following methods /^(is)([a-z]+)(To)?$/
164 *
165 * @param string $name
166 * @param array $args
167 *
168 * @return boolean|null
169 *
170 * @since 6.9.25 https://github.com/aamplugin/advanced-access-manager/issues/353
171 * @since 6.9.24 Initial implementation of the method
172 *
173 * @access public
174 * @version 6.9.25
175 */
176 public function __call($name, $args)
177 {
178 $result = null;
179
180 // We are calling method like isAllowed, isAttached or isDeniedTo
181 if (strpos($name, 'is') === 0) {
182 $resource = array_shift($args);
183
184 if (strpos($name, 'To') === (strlen($name) - 2)) {
185 $effect = substr($name, 2, -2);
186 $action = array_shift($args);
187 } else {
188 $effect = substr($name, 2);
189 $action = null;
190 }
191
192 $context_args = array_shift($args);
193
194 // Method overload. If the next argument is boolean, then this indicates
195 // the $default response. Otherwise, the $default is null and whatever is
196 // in the $context_args is considered to be actual inline args
197 if (is_bool($context_args)) {
198 $default = $context_args;
199 $context_args = array_shift($args);
200 } else {
201 $default = null;
202 }
203
204 $result = $this->is(
205 $resource,
206 $this->_stemEffect($effect),
207 $action,
208 $default,
209 is_array($context_args) ? $context_args : array()
210 );
211 }
212
213 return $result;
214 }
215
216 /**
217 * Get policy parameter
218 *
219 * @param string $name
220 * @param array $args
221 *
222 * @return mixed
223 *
224 * @since 6.4.1 https://github.com/aamplugin/advanced-access-manager/issues/84
225 * @since 6.4.0 Supporting "Value" to be an array
226 * @since 6.0.0 Initial implementation of the method
227 *
228 * @access public
229 * @version 6.4.1
230 */
231 public function getParam($id, $args = array())
232 {
233 $value = null;
234
235 if (isset($this->tree['Param'][$id])) {
236 $param = $this->getBestCandidate($this->tree['Param'][$id], $args);
237 $value = is_null($param) ? null : $param['Value'];
238 }
239
240 return $value;
241 }
242
243 /**
244 * Find all params that match provided search criteria
245 *
246 * @param string|array $s
247 * @param array $args
248 *
249 * @return array
250 *
251 * @since 6.4.1 https://github.com/aamplugin/advanced-access-manager/issues/84
252 * @since 6.0.0 Initial implementation of the method
253 *
254 * @access public
255 * @version 6.4.1
256 */
257 public function getParams($s, $args = array())
258 {
259 if (is_array($s)) {
260 $regex = '/^(' . implode('|', $s) . ')$/i';
261 } else {
262 $regex = "/^{$s}$/i";
263 }
264
265 $params = array();
266
267 foreach (array_keys($this->tree['Param']) as $id) {
268 if (preg_match($regex, $id)) {
269 $params[$id] = $this->getBestCandidate(
270 $this->tree['Param'][$id], $args
271 );
272 }
273 }
274
275 return $params;
276 }
277
278 /**
279 * Find all statements that match provided resource of list of resources
280 *
281 * @param string|array $s
282 * @param array $args
283 *
284 * @return array
285 *
286 * @since 6.5.3 https://github.com/aamplugin/advanced-access-manager/issues/124
287 * @since 6.4.1 https://github.com/aamplugin/advanced-access-manager/issues/84
288 * @since 6.0.0 Initial implementation of the method
289 *
290 * @access public
291 * @version 6.5.3
292 */
293 public function getResources($s, $args = array())
294 {
295 if (is_array($s)) {
296 $regex = '/^(' . implode('|', $s) . '):/i';
297 } else {
298 $regex = "/^{$s}:/i";
299 }
300
301 $statements = array();
302
303 foreach ($this->tree['Statement'] as $key => $stms) {
304 if (preg_match($regex, $key)) {
305 $stm = $this->getBestCandidate($stms, $args);
306
307 if (!is_null($stm)) {
308 // Remove the resource type to keep it clean
309 $statements[preg_replace($regex, '', $key)] = $stm;
310 }
311 }
312 }
313
314 return $statements;
315 }
316
317 /**
318 * Hook into WP core function to override WP options
319 *
320 * @param mixed $res
321 * @param string $option
322 *
323 * @return mixed
324 *
325 * @since 6.0.4 Fixed the potential bug with improperly merged options when Value
326 * is defined as multi-dimensional array
327 * @since 6.0.0 Initial implementation of the method
328 *
329 * @access public
330 * @version 6.0.4
331 */
332 public function getOption($res, $option)
333 {
334 if (isset($this->tree['Param']["option:{$option}"])) {
335 $param = $this->getBestCandidate(
336 $this->tree['Param']["option:{$option}"]
337 );
338 }
339
340 if (is_null($param)) {
341 $res = null;
342 } elseif (is_array($res) && is_array($param['Value'])) {
343 $res = array_replace_recursive($res, $param['Value']);
344 } else {
345 $res = $param['Value'];
346 }
347
348 return $res;
349 }
350
351 /**
352 * Get parsed policy tree
353 *
354 * @return array
355 *
356 * @access public
357 * @version 6.0.0
358 */
359 public function getTree()
360 {
361 return $this->tree;
362 }
363
364 /**
365 * Check if resource and/or action is allowed
366 *
367 * @param mixed $resource Resource name or resource object
368 * @param string $effect Constraint effect (e.g. allow, deny)
369 * @param string $action Any specific action upon provided resource
370 * @param bool|null $default Default response
371 * @param array $args Inline arguments that are added to the context
372 *
373 * @return boolean|null The `null` is returned if there is no applicable statements
374 * that explicitly define effect
375 *
376 * @access protected
377 * @version 6.9.24
378 */
379 protected function is($resource, $effect, $action, $default, $args)
380 {
381 $result = $default;
382 $id = strtolower($resource . (!empty($action) ? ':' . $action : ''));
383
384 if (isset($this->tree['Statement'][$id])) {
385 $stm = $this->getBestCandidate(
386 $this->tree['Statement'][$id], $args
387 );
388
389 if (!is_null($stm)) {
390 $result = (strtolower($stm['Effect']) === $effect);
391 }
392 }
393
394 return $result;
395 }
396
397 /**
398 * Stem the effect
399 *
400 * Basically try to stem the effect from something like "Allowed" to "allow", or
401 * "Denied" to "deny".
402 *
403 * @param string $effect
404 *
405 * @return string
406 *
407 * @access private
408 * @version 6.9.24
409 */
410 private function _stemEffect($effect)
411 {
412 $n = strtolower($effect);
413
414 return (isset($this->_effects[$n]) ? $this->_effects[$n] : $n);
415 }
416
417 /**
418 * Based on multiple competing statements or params, get the best candidate
419 *
420 * @param array $candidates
421 * @param array $args
422 *
423 * @return array|null
424 *
425 * @since 6.9.25 https://github.com/aamplugin/advanced-access-manager/issues/355
426 * @since 5.5.4 https://github.com/aamplugin/advanced-access-manager/issues/128
427 * @since 6.5.3 Initial implementation of the method
428 *
429 * @access protected
430 * @version 6.9.25
431 */
432 protected function getBestCandidate($candidates, $args = array())
433 {
434 $candidate = null;
435
436 if (is_array($candidates) && isset($candidates[0])) {
437 // Take in consideration ONLY currently applicable candidates and select
438 // either the last one or the one that is enforced
439 $enforced = false;
440
441 foreach($candidates as $c) {
442 if ($this->isApplicable($c, $args)) {
443 if (!empty($c['Enforce'])) {
444 $candidate = $c;
445 $enforced = true;
446 } elseif ($enforced === false) {
447 $candidate = $c;
448 }
449 }
450 }
451 } else if ($this->isApplicable($candidates, $args)) {
452 $candidate = $candidates;
453 }
454
455 return $candidate;
456 }
457
458 /**
459 * Parse JSON policy and extract statements and params
460 *
461 * @param WP_Post $policy
462 *
463 * @return array
464 *
465 * @since 6.2.1 Added support for the POLICY_META token
466 * @since 6.0.0 Initial implementation of the method
467 *
468 * @access protected
469 * @version 6.2.1
470 */
471 protected function parsePolicy($policy)
472 {
473 // Any ${POLICY_META. replace with ${POLICY_META.123
474 $json = str_replace(
475 '${POLICY_META.',
476 '${POLICY_META.' . $policy->ID . '.',
477 $policy->post_content
478 );
479 $val = json_decode($json, true);
480
481 // Do not load the policy if any errors
482 if (json_last_error() === JSON_ERROR_NONE) {
483 $tree = array(
484 'Statement' => $this->_getArrayOfArrays($val, 'Statement'),
485 'Param' => $this->_getArrayOfArrays($val, 'Param'),
486 );
487 } else {
488 $tree = array('Statement' => array(), 'Param' => array());
489
490 // Make sure that this is noticed
491 _doing_it_wrong(
492 __CLASS__ . '::' . __METHOD__,
493 sprintf(
494 'Access policy %d error %s', $policy->ID, json_last_error_msg()
495 ),
496 AAM_VERSION
497 );
498 }
499
500 return $tree;
501 }
502
503 /**
504 * Get array of array for Statement and Param policy props
505 *
506 * @param array $input
507 * @param string $prop
508 *
509 * @return array
510 *
511 * @access private
512 * @version 6.0.0
513 */
514 private function _getArrayOfArrays($input, $prop)
515 {
516 $response = array();
517
518 // Parse Statements and determine if it is multidimensional
519 if (array_key_exists($prop, $input)) {
520 if (!isset($input[$prop][0]) || !is_array($input[$prop][0])) {
521 $response = array($input[$prop]);
522 } else {
523 $response = $input[$prop];
524 }
525 }
526
527 return $response;
528 }
529
530 /**
531 * Extend tree with additional statements and params
532 *
533 * @param array $addition
534 *
535 * @return array
536 *
537 * @since 6.9.25 https://github.com/aamplugin/advanced-access-manager/issues/355
538 * @since 6.5.3 https://github.com/aamplugin/advanced-access-manager/issues/122
539 * https://github.com/aamplugin/advanced-access-manager/issues/124
540 * @since 6.4.1 Simplified by removing &$tree first param
541 * @since 6.4.0 Supporting Param's Value to be more than just a scalar value
542 * @since 6.2.1 Typecasting param's value
543 * @since 6.1.0 Added support for the `=>` (map to) operator
544 * @since 6.0.0 Initial implementation of the method
545 *
546 * @access protected
547 * @version 6.9.25
548 */
549 protected function updatePolicyTree($addition)
550 {
551 $stmts = &$this->tree['Statement'];
552 $params = &$this->tree['Param'];
553
554 $callback = array($this, 'getOption'); // Callback that hooks into get_option
555
556 // Step #1. If there are any params, let's index them and insert into the list
557 foreach ($addition['Param'] as $param) {
558 if (!empty($param['Key'])) {
559 $param['Value'] = $this->replaceTokens($param['Value'], true);
560
561 foreach($this->evaluatePolicyKey($param['Key']) as $key) {
562 if (!isset($params[$key]) || empty($params[$key]['Enforce'])) {
563 if (!isset($params[$key])) {
564 $params[$key] = array();
565 }
566
567 array_push($params[$key], $param);
568
569 // If "option:" - hooks to the WP core for override
570 if (strpos($key, 'option:') === 0) {
571 $name = substr($key, 7);
572
573 // Hook into the core
574 add_filter('pre_option_' . $name, $callback, 1, 2);
575 add_filter('pre_site_option_' . $name, $callback, 1, 2);
576 }
577 }
578 }
579 }
580 }
581
582 // Step #2. If there are any statements, let's index them by resource:action
583 // and insert into the list of statements
584 foreach ($addition['Statement'] as $stm) {
585 $resources = (isset($stm['Resource']) ? (array) $stm['Resource'] : array());
586 $actions = (isset($stm['Action']) ? (array) $stm['Action'] : array(''));
587
588 foreach ($resources as $res) {
589 foreach($this->evaluatePolicyKey($res) as $resource) {
590 foreach ($actions as $act) {
591 $id = strtolower($resource . (!empty($act) ? ":{$act}" : ''));
592
593 if (!isset($stmts[$id])) {
594 $stmts[$id] = array();
595 }
596
597 array_push($stmts[$id], $stm);
598 }
599 }
600 }
601 }
602 }
603
604 /**
605 * Evaluate resource name or param key
606 *
607 * The resource or param key may have tokens that build dynamic keys. This method
608 * covers 3 possible scenario:
609 * - Map To "=>" - the token should return array of values that are mapped to the
610 * key;
611 * - Token - returns scalar value;
612 * - Raw Value - returns as-is
613 *
614 * @param string $key
615 *
616 * @return array
617 *
618 * @access protected
619 * @version 6.4.1
620 */
621 protected function evaluatePolicyKey($key)
622 {
623 $response = array();
624
625 // Allow to build resource name or param key dynamically.
626 if (preg_match('/^(.*)[\s]+(map to|=>)[\s]+(.*)$/i', $key, $match)) {
627
628 // e.g. "Term:category:%s:posts => ${USER_META.regions}"
629 // e.g. "%s:default:category => ${HTTP_POST.post_types}"
630 $values = (array) AAM_Core_Policy_Token::getTokenValue($match[3]);
631
632 // Create the map of resources/params and replace
633 foreach($values as $value) {
634 $response[] = sprintf($match[1], $value);
635 }
636 } elseif (preg_match_all('/(\$\{[^}]+\})/', $key, $match)) {
637 // e.g. "Term:category:${USER_META.region}:posts"
638 $response = array(AAM_Core_Policy_Token::evaluate($key, $match[1]));
639 } else {
640 $response = array($key);
641 }
642
643 return $response;
644 }
645
646 /**
647 * Replace all the dynamic tokens recursively
648 *
649 * @param array $data
650 * @param boolean $type_cast
651 *
652 * @return array
653 *
654 * @since 6.4.1 Added type casting param
655 * @since 6.0.0 Initial implementation of the method
656 *
657 * @access protected
658 * @version 6.4.1
659 */
660 protected function replaceTokens($data, $type_cast = false)
661 {
662 $replaced = array();
663
664 if (is_scalar($data)) {
665 $replaced = $this->_replaceTokensInString($data, $type_cast);
666 } else {
667 foreach($data as $key => $value) {
668 // Evaluate array's key and replace tokens
669 $key = $this->_replaceTokensInString($key);
670
671 // Evaluate array's value and replace tokens
672 if (is_array($value)) {
673 $replaced[$key] = $this->replaceTokens($value, $type_cast);
674 } else {
675 $replaced[$key] = $this->_replaceTokensInString(
676 $value, $type_cast
677 );
678 }
679 }
680 }
681
682 return $replaced;
683 }
684
685 /**
686 * Replace tokens is provided scalar string
687 *
688 * @param string $token
689 * @param boolean $type_cast
690 *
691 * @return mixed
692 *
693 * @access private
694 * @version 6.4.1
695 */
696 private function _replaceTokensInString($token, $type_cast = false)
697 {
698 if (preg_match_all('/(\$\{[^}]+\})/', $token, $match)) {
699 $value = AAM_Core_Policy_Token::evaluate($token, $match[1]);
700
701 if ($type_cast === true) {
702 $replaced = AAM_Core_Policy_Typecast::execute($value);
703 } else {
704 $replaced = $value;
705 }
706 } else {
707 $replaced = $token;
708 }
709
710 return $replaced;
711 }
712
713 /**
714 * Perform some internal clean-up
715 *
716 * @param array &$statements
717 *
718 * @return void
719 *
720 * @since 6.5.3 https://github.com/aamplugin/advanced-access-manager/issues/124
721 * @since 6.0.0 Initial implementation of the method
722 *
723 * @access private
724 * @version 6.5.3
725 */
726 private function _cleanupTree(&$statements)
727 {
728 foreach($statements as $id => &$stm) {
729 if (is_array($stm) && isset($stm[0])) {
730 $this->_cleanupTree($stm);
731 } else {
732 if (isset($stm['Resource'])) {
733 unset($statements[$id]['Resource']);
734 }
735 if (isset($stm['Action'])) {
736 unset($statements[$id]['Action']);
737 }
738 }
739 }
740 }
741
742 /**
743 * Check if policy block is applicable
744 *
745 * @param array $block
746 * @param array $args
747 *
748 * @return boolean
749 *
750 * @access protected
751 * @version 6.0.0
752 */
753 protected function isApplicable($block, $args = array())
754 {
755 $result = true;
756
757 if (!empty($block['Condition']) && is_array($block['Condition'])) {
758 $result = AAM_Core_Policy_Condition::getInstance()->evaluate(
759 $block['Condition'], $args
760 );
761 }
762
763 return $result;
764 }
765
766 }