/
home
/
assocoweys
/
marina
/
wp-content
/
plugins
/
advanced-access-manager
/
application
/
Framework
/
Service
/
Upload File
HOME
<?php /** * ====================================================================== * LICENSE: This file is subject to the terms and conditions defined in * * file 'license.txt', which is part of this source code package. * * ====================================================================== */ /** * JSON access policies service * * @package AAM * @version 7.0.0 */ class AAM_Framework_Service_Policies { use AAM_Framework_Service_BaseTrait; /** * JSON Access policy CPT * * @version 7.0.0 */ const CPT = 'aam_policy'; /** * List of all registered policies for given access level * * @var array * @access private * * @version 7.0.0 */ private $_registered_policies = null; /** * Cache policy tree * * To improve performance, caching parsed policy tree * * @var array * @access private * * @version 7.0.0 */ private $_cached_policy_tree = null; /** * Get list of access policies * * @param array $args [Optional] * * @return array * @access public * * @version 7.0.0 */ public function get_policies($args = []) { try { $result = $this->_get_registered_policies($args); } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Alias for the get_policies method * * @param array $args * * @return array * @access public * * @version 7.0.0 */ public function policies($args = []) { return $this->get_policies($args); } /** * Get a single registered policy * * @param int $policy_id * * @return array * @access public * * @version 7.0.0 */ public function get_policy($policy_id) { try { $match = array_filter( $this->_get_registered_policies(), function($p) use ($policy_id) { return $p === $policy_id; }, ARRAY_FILTER_USE_KEY ); if (!empty($match)) { $result = array_shift($match); } else { throw new OutOfRangeException( sprintf('Policy with ID %d does not exist', $policy_id) ); } } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Alias for the get_policy method * * @param int $policy_id * * @return array * @access public * * @version 7.0.0 */ public function policy($policy_id) { return $this->get_policy($policy_id); } /** * Attach a policy to the access level * * @param int $policy_id * * @return bool * @access public * * @version 7.0.0 */ public function attach($policy_id) { try { // Attach new policy to the list $result = $this->_update($policy_id, 'attach'); if ($result) { $this->_registered_policies[$policy_id] = $this->_prepare_policy_item( get_post($policy_id) ); // Reset internal cache $this->_cached_policy_tree = null; } } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Detach a policy from the access level * * @param int $policy_id * * @return bool * @access public * * @version 7.0.0 */ public function detach($policy_id) { try { // Detach new policy to the list $result = $this->_update($policy_id, 'detach'); if ($result) { $this->_registered_policies[$policy_id] = $this->_prepare_policy_item( get_post($policy_id) ); // Reset internal cache $this->_cached_policy_tree = null; } } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Create new policy and apply it to current access level if specified * * @param string|array $policy * @param string $status [Optional] * @param bool $apply [Optional] * * @return int|WP_Error * @access public * * @version 7.0.0 */ public function create($policy, $status = 'publish', $apply = true) { try { $post_data = []; // Let's validate the incoming policy first if (is_string($policy)) { $post_content = $policy; } elseif (is_array($policy) && isset($policy['json']) ) { $post_content = $policy['json']; $post_data = [ 'post_title' => isset($policy['title']) ? $policy['title']: '', 'post_excerpt' => isset($policy['excerpt']) ? $policy['excerpt']: '', ]; } else { throw new InvalidArgumentException('Invalid policy provided'); } $decoded = json_decode( htmlspecialchars_decode(stripslashes($post_content)), true ); if (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidArgumentException( 'The provided policy is not a valid JSON' ); } if (is_multisite()) { switch_to_blog($this->_get_main_site_id()); } // Insert new policy $result = wp_insert_post(array_merge( [ 'post_status' => $status ], $post_data, [ 'post_type' => self::CPT, 'post_content' => wp_json_encode($decoded, JSON_PRETTY_PRINT) ] )); if (is_multisite()) { restore_current_blog(); } if (is_wp_error($result)) { throw new RuntimeException($result->get_error_message()); } elseif ($apply) { if (!$this->_update($result, 'attach')) { throw new RuntimeException('Failed to attach created policy'); } // Insert policy into the list of registered policies $this->_registered_policies[$result] = $this->_prepare_policy_item( get_post($result) ); // Reset internal cache $this->_cached_policy_tree = null; } } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Get list of applicable statements for given resource key * * The resource_key can be a full resource name or part of it. RegEx is used * to find a match * * @param string $resource_pattern [Optional] * @param array $args [Optional] * * @return array * @access public * * @version 7.0.0 */ public function get_statements($resource_pattern = null, $args = []) { try { $result = []; $matches = $this->_search('statement', $resource_pattern, $args); foreach($matches as $key => $stm) { $result[$key] = array_filter($stm, function($k) { return !in_array($k, [ 'Condition' ], true); }, ARRAY_FILTER_USE_KEY); } } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Get list of applicable params for given param key * * The param_key can be a full param name or part of it. RegEx is used * to find a match * * @param string $key_pattern [Optional] * @param array $args [Optional] * * @return array * @access public * * @version 7.0.0 */ public function get_params($key_pattern = null, $args = []) { try { $result = []; $matches = $this->_search('param', $key_pattern, $args); foreach($matches as $key => $param) { $result[$key] = $param['Value']; } } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Get a single param * * The $key has to be an exact match for the parameter, otherwise the WP_Error is * returned. * * @param string $key * @param array $args [Optional] * * @return mixed|WP_Error * @access public * * @version 7.0.0 */ public function get_param($key, $args = []) { $result = null; try { foreach ($this->_get_processed_params($args) as $param_key => $params) { if ($param_key === $key) { $candidate = $this->_get_best_candidate($params, $args); if (!is_null($candidate)) { $result = $candidate['Value']; break; // No need to search any further } } } } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Alias for the get_statements method * * @param string $resource_pattern [Optional] * @param array $args [Optional] * * @return array * @access public * * @version 7.0.0 */ public function statements($resource_pattern = null, $args = []) { return $this->get_statements($resource_pattern, $args); } /** * Alias for the get_params method * * @param string $key_pattern [Optional] * @param array $args [Optional] * * @return array * @access public * * @version 7.0.0 */ public function params($key_pattern = null, $args = []) { return $this->get_params($key_pattern, $args); } /** * Alias for the get_param method * * The $key has to be an exact match for the parameter, otherwise the WP_Error is * returned. * * @param string $key * @param array $args [Optional] * * @return mixed|WP_Error * @access public * * @version 7.0.0 */ public function param($key, $args = []) { return $this->get_param($key, $args); } /** * Reset attached policies for current access level * * @return bool * @access public * * @version 7.0.0 */ public function reset() { try { $result = $this->_get_resource()->reset(); } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Are permissions customized for current access level * * Determine if permissions for the resource are customized for the current * access level. Permissions are considered customized if there is at least one * permission explicitly allowed or denied. * * @return boolean * @version 7.0.0 */ public function is_customized() { try { $result = $this->_get_resource()->is_customized(); } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Determine if policy is attached to current access level * * @param int $policy_id * * @return bool * @access public * * @version 7.0.0 */ public function is_attached($policy_id) { try { $result = $this->_is_attached($policy_id); } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Determine if policy is detached from current access level * * @param int $policy_id * * @return bool * @access public * * @version 7.0.0 */ public function is_detached($policy_id) { try { $result = !$this->_is_attached($policy_id); } catch (Exception $e) { $result = $this->_handle_error($e); } return $result; } /** * Get list of all registered access policies on the site * * @param array $args [Optional] * * @return array * @access private * * @version 7.0.0 */ private function _get_registered_policies($args = []) { if (is_null($this->_registered_policies)) { // Initializing the registered policies cache $this->_registered_policies = []; // Fetching the list of registered policies from DB $policies = get_posts(array_merge( [ 'post_status' => [ 'publish', 'draft', 'pending', 'private' ], 'nopaging' => true, 'order' => 'ASC', 'orderby' => 'post_parent, menu_order' ], $args, [ 'suppress_filters' => true, 'post_type' => self::CPT ] )); foreach($policies as $policy) { $this->_registered_policies[$policy->ID] = $this->_prepare_policy_item( $policy ); } } return $this->_registered_policies; } /** * Access policy tree for given access level * * @param AAM_Framework_AccessLevel_Interface $access_level * * @return array * @access private * * @version 7.0.0 */ private function _get_access_level_policy_tree() { if (is_null($this->_cached_policy_tree)) { // Reset policy tree each time we get it $result = [ 'Statement' => [], 'Param' => [] ]; // Important! Retain the exact order of fetched policies $permissions = $this->_get_resource()->get_permissions(); foreach($this->_get_registered_policies() as $id => $policy) { if (array_key_exists($id, $permissions) && $permissions[$id]['attach']['effect'] !== 'detach' ) { $this->_insert_policy_into_tree($policy, $result); } } // Cache the tree $this->_cached_policy_tree = $result; } else { $result = $this->_cached_policy_tree; } return $result; } /** * Get access policy resource * * @return AAM_Framework_Resource_Policy * @access private * * @version 7.0.0 */ private function _get_resource() { return $this->_get_access_level()->get_resource( AAM_Framework_Type_Resource::POLICY ); } /** * Parse access policy content and convert it to a normalized tree * * @param WP_Post $policy * * @return array * @access private * * @version 7.0.5 */ private function _parse_policy($policy) { // Parsing policy raw content // Any ${POLICY_META. replace with ${POLICY_META.123 $raw = json_decode(str_replace( '${POLICY_META.', '${POLICY_META.' . $policy->ID . '.', $policy->post_content ), true); // Do not load the policy if any errors if (json_last_error() === JSON_ERROR_NONE && is_array($raw)) { $result = [ 'Statement' => $this->_get_array_of_arrays($raw, 'Statement'), 'Param' => $this->_get_array_of_arrays($raw, 'Param'), ]; } else { $result = [ 'Statement' => [], 'Param' => [] ]; // Make sure that this is noticed _doing_it_wrong( __CLASS__ . '::' . __METHOD__, sprintf('Invalid access policy (ID: %d)', $policy->ID), AAM_VERSION ); } return $result; } /** * Get array of array for Statement and Param policy props * * @param array $input * @param string $prop * * @return array * @access private * * @version 7.0.0 */ private function _get_array_of_arrays($input, $prop) { $response = []; // Parse Statements and determine if it is multidimensional if (array_key_exists($prop, $input)) { if (!isset($input[$prop][0]) || !is_array($input[$prop][0])) { $response = array($input[$prop]); } else { $response = $input[$prop]; } } return $response; } /** * Search statement of param by given key * * @param string $type * @param string $pattern * @param array $args * * @return array * @access private * * @version 7.0.0 */ private function _search($type, $pattern, $args) { $result = []; if (is_string($pattern)) { $regex = '/^' . str_replace('\*', '(.*)', preg_quote($pattern)) . '/i'; } else { $regex = null; } if ($type === 'statement') { $list = $this->_get_processed_statements($args); } else { $list = $this->_get_processed_params($args); } foreach ($list as $key => $candidates) { if (is_null($regex) || preg_match($regex, $key)) { $candidate = $this->_get_best_candidate($candidates, $args); if (!is_null($candidate)) { $result[$key] = $candidate; } } } return $result; } /** * Get all statements associated with current access level * * This method dynamically replaces all the markers * * @return array * @access private * * @version 7.0.0 */ private function _get_processed_statements($args) { $tree = $this->_replace_dynamic_markers( $this->_get_access_level_policy_tree(), $args ); // Dynamically replacing all the markers return $tree['Statement']; } /** * Get all params associated with current access level * * This method dynamically replaces all the markers * * @return array * @access private * * @version 7.0.0 */ private function _get_processed_params($args) { $tree = $this->_replace_dynamic_markers( $this->_get_access_level_policy_tree(), $args ); // Dynamically replacing all the markers return $tree['Param']; } /** * Recursively iterate over the tree and replace dynamic markers * * @param array $tree * @param array $args * * @return array * @access private * * @version 7.0.0 */ private function _replace_dynamic_markers($tree, $args) { // Step #1. We are building the tree of params & statements. Params go first // because they can be used as markers for statements $params = []; foreach ($tree['Param'] as $param) { if (!empty($param['Key']) && !empty($param['Value'])) { foreach($this->_evaluate_policy_key($param['Key']) as $key) { if (!array_key_exists($key, $params)) { $params[$key] = []; } // Remove the Key and let's dynamically replace all other markers array_push($params[$key], $this->_replace_markers_recursively( $param, $args, [ 'Key', 'Condition' ] )); } } } // Step #2. If there are any statements, let's index them by resource:action // and insert into the list of statements $statements = []; foreach ($tree['Statement'] as $stm) { $resources = (isset($stm['Resource']) ? (array) $stm['Resource'] : []); $actions = (isset($stm['Action']) ? (array) $stm['Action'] : [ '' ]); foreach ($resources as $res) { foreach($this->_evaluate_policy_key($res) as $resource) { foreach ($actions as $action) { $key = strtolower( $resource . (!empty($action) ? ":{$action}" : '') ); if (!array_key_exists($key, $statements)) { $statements[$key] = []; } // Process the statement $statement = $this->_replace_markers_recursively( $stm, $args, [ 'Resource', 'Action', 'Condition' ] ); // Making sure we have a single representation of the // Resource attribute $statement['Resource'] = $resource; // If there is a specific action applied to the statement's // resource, include it in the statement if (!empty($action)){ $statement['Action'] = $action; } array_push($statements[$key], $statement); } } } } return [ 'Statement' => $statements, 'Param' => $params ]; } /** * Replace dynamic markers recursively * * @param array $array * @param array $args * @param array $skip_keys [Optional] * * @return array * @access private * * @version 7.0.0 */ private function _replace_markers_recursively( $array, $args, $skip_keys = [] ) { $result = []; foreach($array as $key => $value) { if (in_array($key, $skip_keys, true)) { $result[$key] = $value; // Leave the entire branch as-is } else { // Note! We do not type-cast key because it has to be a scalar // value for all this thing to work. Type-casting for a key can // be done only inside Condition block as it it handled // differently $key = $this->_replace_markers($key, false, $args); if (is_array($value)) { $result[$key] = $this->_replace_markers_recursively( $value, $args ); } else { $result[$key] = $this->_replace_markers($value, true, $args); } } } return $result; } /** * Evaluate resource name or param key * * The resource or param key may have tokens that build dynamic keys. This method * covers 3 possible scenario: * - Map To "=>" - the token should return array of values that are mapped to the * key; * - Token - returns scalar value; * - Raw Value - returns as-is * * @param string $key * * @return array * @access private * * @version 7.0.0 */ private function _evaluate_policy_key($key) { $result = []; // Allow to build resource name or param key dynamically. if (preg_match('/^(.*)[\s]+(map to|=>)[\s]+(.*)$/i', $key, $match)) { // e.g. "Term:category:%s:posts => ${USER_META.regions}" // e.g. "%s:default:category => ${HTTP_POST.post_types}" $values = (array) AAM_Framework_Policy_Marker::get_marker_value( $match[3] ); // Create the map of resources/params and replace foreach($values as $value) { $result[] = sprintf($match[1], $value); } } else { $result[] = AAM_Framework_Policy_Marker::execute($key, [], false); } return $result; } /** * Replace all the dynamic markers recursively * * @param array $data * @param bool $type_cast [Optional] * @param array $args [Optional] * * @return array * @access private * * @version 7.0.0 */ private function _replace_markers($data, $type_cast = false, $args = []) { $replaced = array(); if (is_scalar($data)) { $replaced = AAM_Framework_Policy_Marker::execute( $data, $args, $type_cast ); } else { foreach($data as $key => $value) { // Evaluate array's key and replace markers $key = AAM_Framework_Policy_Marker::execute($key, $args, false); // Evaluate array's value and replace markers if (is_array($value)) { $replaced[$key] = $this->_replace_markers( $value, $type_cast, $args ); } else { $replaced[$key] = AAM_Framework_Policy_Marker::execute( $value, $args, $type_cast ); } } } return $replaced; } /** * Based on multiple competing statements or params, get the best candidate * * @param array $candidates * @param array $args * * @return array|null * @access private * * @version 7.0.0 */ private function _get_best_candidate($candidates, $args) { $result = null; // Take in consideration ONLY currently applicable candidates and select // either the last one or the one that is enforced $enforced = false; foreach($candidates as $candidate) { if ($this->_is_applicable($candidate, $args)) { if (!empty($candidate['Enforce'])) { $result = $candidate; $enforced = true; } elseif ($enforced === false) { $result = $candidate; } } } return $result; } /** * Check if policy block is applicable * * @param array $block * @param array $args * * @return boolean * @access private * * @version 7.0.0 */ private function _is_applicable($block, $args) { $result = true; if (!empty($block['Condition']) && is_array($block['Condition'])) { $result = AAM_Framework_Policy_Condition::get_instance()->execute( $block['Condition'], $args ); } return $result; } /** * Update policy container with new policy effect * * @param int $policy_id * @param string $effect * * @return bool * @access private * * @version 7.0.0 */ private function _update($policy_id, $effect) { return $this->_get_resource()->set_permission( $this->_normalize_resource_identifier($policy_id), 'attach', $effect ); } /** * Prepare policy item * * @param WP_Post $policy * * @return array * @access private * * @version 7.0.0 */ private function _prepare_policy_item($policy) { return [ 'id' => $policy->ID, 'status' => $policy->post_status, 'json' => $policy->post_content, 'parsed' => $this->_parse_policy($policy), 'is_attached' => $this->_is_attached($policy->ID), 'ref' => $policy ]; } /** * Determine if policy is attached to current access level * * @param int $policy_id * * @return bool * @access private * * @version 7.0.0 */ private function _is_attached($policy_id) { $permission = $this->_get_resource()->get_permission( $this->_normalize_resource_identifier($policy_id), 'attach' ); if (!empty($permission)) { $result = $permission['effect'] !== 'detach'; } else { $result = false; } return $result; } /** * Insert a policy into a policy tree * * The policy tree represents a collection of statements and params from * published policies that are attached to current access level * * @param array $policy * @param array &$policy_tree * * @return void * @access private * * @version 7.0.0 */ private function _insert_policy_into_tree($policy, &$policy_tree) { if ($policy['status'] === 'publish') { $policy_tree = [ 'Statement' => array_merge( $policy_tree['Statement'], $policy['parsed']['Statement'] ), 'Param'=> array_merge( $policy_tree['Param'], $policy['parsed']['Param'] ) ]; } } /** * @inheritDoc * @return WP_Post * * @version 7.0.6 */ private function _normalize_resource_identifier($resource_identifier) { $result = null; if (is_a($resource_identifier, WP_Post::class)) { $result = $resource_identifier; } elseif (is_numeric($resource_identifier)) { $result = get_post($resource_identifier); } elseif (is_array($resource_identifier)) { if (isset($resource_identifier['id'])) { $result = get_post($resource_identifier['id']); } else { // Let's get post_name if (isset($resource_identifier['slug'])) { $post_name = $resource_identifier['slug']; } elseif (isset($resource_identifier['post_name'])) { $post_name = $resource_identifier['post_name']; } if (!empty($post_name)) { $result = $this->misc->get_post_by_slug($post_name, self::CPT); } } // Do some additional validation if id & post_type are provided in the // array if (is_a($result, WP_Post::class) && self::CPT !== $result->post_type ) { throw new OutOfRangeException('Invalid policy instance'); } } if (!is_a($result, WP_Post::class)) { throw new OutOfRangeException('The resource identifier is invalid'); } return $result; } /** * Get main site ID in multi-site setup * * @return int * @access private * * @version 7.0.0 */ private function _get_main_site_id() { if (function_exists('get_main_site_id')) { $result = get_main_site_id(); } elseif (is_multisite()) { $network = get_network(); $result = ($network ? $network->site_id : 0); } else { $result = get_current_blog_id(); } return $result; } }