File "Content.php"
Full Path: /home/rubycabfbt/www/wp-content/plugins/advanced-access-manager/application/Service/Content.php
File size: 24.66 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* ======================================================================
* LICENSE: This file is subject to the terms and conditions defined in *
* file 'license.txt', which is part of this source code package. *
* ======================================================================
*/
/**
* Content (aka Posts & Terms) service
*
* @package AAM
* @version 7.0.0
*/
class AAM_Service_Content
{
use AAM_Service_BaseTrait;
/**
* Default configurations
*
* @version 7.0.6
*/
const DEFAULT_CONFIG = [
'service.post_types.manage_all' => false,
'service.taxonomies.manage_all' => false,
'service.posts.decorate_teaser' => true
];
/**
* Collection of post type caps
*
* This is a collection of post type capabilities for optimization reasons. It
* is used by _map_meta_cap method to determine if additional check needs to be
* perform
*
* @var array
* @access private
*
* @version 7.0.0
*/
private $_content_capabilities = array(
'edit_post', 'edit_page', 'read_post', 'read_page', 'publish_post'
);
/**
* Constructor
*
* @return void
* @access protected
*
* @version 7.0.4
*/
protected function __construct()
{
add_filter('aam_get_config_filter', function($result, $key) {
if (empty($result) && array_key_exists($key, self::DEFAULT_CONFIG)) {
$result = self::DEFAULT_CONFIG[$key];
}
return $result;
}, 10, 2);
// Register RESTful API
AAM_Restful_Content::bootstrap();
add_action('init', function() {
$this->initialize_hooks();
}, PHP_INT_MAX);
}
/**
* Initialize Content service hooks
*
* @return void
* @access protected
*
* @version 7.0.4
*/
protected function initialize_hooks()
{
if (is_admin()) {
// Hook that initialize the AAM UI part of the service
add_action('aam_initialize_ui_action', function () {
AAM_Backend_Feature_Main_Content::register();
});
// Check if Access Manager metabox feature is enabled
$metaboxEnabled = AAM::api()->config->get(
'core.settings.ui.render_access_metabox'
);
if ($metaboxEnabled) {
// Register custom access control metabox
add_action(
'add_meta_boxes',
function() {
$this->_register_access_manager_metabox();
}
);
}
}
if (!is_admin()) {
// Password protected filter
add_filter('post_password_required', function($result, $post) {
return $this->_is_password_protected($result, $post);
}, 10, 2);
// Manage password check expiration
add_filter('post_password_expires', function($result) {
return $this->_post_password_expires($result);
});
// Filter navigation pages & taxonomies
add_filter('wp_get_nav_menu_items', function($pages) {
return $this->_get_nav_menu_items($pages);
}, PHP_INT_MAX);
// Manage access to frontend posts & pages
add_action('wp', function() {
global $wp_query;
if (is_single()
|| is_page()
|| is_post_type_archive()
|| $wp_query->is_posts_page
) {
$this->_authorize_post_access();
}
}, PHP_INT_MAX);
}
// Control post visibility
add_filter('posts_clauses_request', function($clauses, $query) {
return $this->_posts_clauses_request($clauses, $query);
}, 10, 2);
// Evaluate if current user can see full content or only a teaser message
add_filter(
'the_content',
function($content) {
return $this->_the_content($content);
}, PHP_INT_MAX
);
// Evaluate if user can comment on a post
add_filter('comments_open', function ($open, $post_id) {
// If Leave Comments option is defined then override the default status.
// Otherwise keep it as-is
if (AAM::api()->posts()->is_denied_to($post_id, 'comment')) {
$open = false;
}
return $open;
}, 10, 2);
// Check if user has ability to perform certain task based on provided
// capability and meta data
add_filter('map_meta_cap', function($caps, $cap, $_, $args) {
return $this->_map_meta_cap($caps, $cap, $args);
}, PHP_INT_MAX, 4);
// REST API action authorization. Triggered before call is dispatched
add_filter(
'rest_request_before_callbacks',
function($response, $_, $request) {
return $this->_rest_request_before_callbacks($response, $request);
}, 10, 3
);
// Audit all registered post types and adjust access controls accordingly
foreach(get_post_types([], 'objects') as $post_type) {
// REST API. Control if user is allowed to publish content
add_filter("rest_pre_insert_{$post_type->name}", function ($post, $request) {
$status = (isset($request['status']) ? $request['status'] : null);
if (in_array($status, array('publish', 'future'), true)) {
$post_id = intval($request['id']);
if (AAM::api()->posts()->is_denied_to($post_id, 'publish')) {
$post = new WP_Error(
'rest_cannot_publish',
'You are not allowed to publish this content',
[ 'status' => rest_authorization_required_code() ]
);
}
}
return $post;
}, 10, 2);
// Populate the collection of post type caps
foreach ([ 'edit_post', 'read_post', 'delete_post', 'publish_posts' ] as $cap) {
$meta_cap = $post_type->cap->{$cap};
if (!empty($meta_cap)
&& !in_array($meta_cap, $this->_content_capabilities, true)
&& ($meta_cap !== 'do_not_allow')
) {
$this->_content_capabilities[] = $cap;
}
}
}
}
/**
* Main frontend access control hook
*
* @return void
*
* @access private
* @global WP_Query $wp_query
*
* @version 7.0.3
*/
private function _authorize_post_access()
{
$post = AAM::api()->misc->get_current_post();
if (!empty($post)) {
$service = AAM::api()->posts();
if ($service->is_restricted($post)) {
AAM::api()->redirect->do_access_denied_redirect();
} elseif ($service->is_redirected($post)) {
AAM::api()->redirect->do_redirect($service->get_redirect($post));
}
}
}
/**
* Check if post is password protected
*
* This callback is used by the Frontend to determine if current post requires
* password in order to see its content
*
* @param boolean $result
* @param WP_Post $post
*
* @return boolean
* @access private
*
* @version 7.0.0
*/
private function _is_password_protected($result, $post)
{
// Honor the manually set password on the post
if (($result === false) && is_a($post, 'WP_Post')) {
$result = is_wp_error($this->_verify_post_password($post));
}
return $result;
}
/**
* Check PASSWORD PROTECTED access option
*
* If post has password set, return WP_Error so the application can do further
* authorization process.
*
* @param WP_Post $post
*
* @return boolean|WP_Error
* @access private
*
* @version 7.0.0
*/
private function _verify_post_password($post)
{
$result = true;
if (AAM::api()->posts()->is_password_protected($post)) {
// Load hash checker
if (!class_exists('PasswordHash')) {
require_once ABSPATH . WPINC . '/class-phpass.php';
}
$checker = new PasswordHash(8, true);
// If password is empty or not provided, try to read it from the cookie.
// This is the default WordPress behavior when it comes to password
// protected posts/pages
$is_matched = $checker->CheckPassword(
AAM::api()->posts()->get_password($post),
wp_unslash(AAM::api()->misc->get(
$_COOKIE, 'wp-postpass_' . COOKIEHASH, ''
))
);
if ($is_matched === false) {
$result = new WP_Error(
'rest_unauthorized',
'The post is password protected. Invalid password provided.',
array('status' => 401)
);
}
}
return $result;
}
/**
* Redefine entered password TTL
*
* @param int $expire
*
* @return int
* @access private
*
* @version 7.0.0
*/
private function _post_password_expires($expire)
{
$ttl = AAM::api()->config->get(
'service.content.password_ttl', null
);
return !empty($ttl) ? time() + strtotime($ttl) : $expire;
}
/**
* Register Access Manager metabox on post edit screen
*
* @return void
* @access private
*
* @version 7.0.0
*/
private function _register_access_manager_metabox()
{
global $post;
if (is_a($post, 'WP_Post')) {
add_meta_box(
'aam-access-manager',
__('Access Manager', 'advanced-access-manager'),
function () {
global $post;
echo AAM_Backend_View::renderPostMetabox($post);
},
null,
'advanced',
'high'
);
}
}
/**
* Filter traditional navigation menu
*
* @param array $pages
*
* @return array
* @access private
*
* @version 7.0.0
*/
private function _get_nav_menu_items($pages)
{
if (is_array($pages)) {
$service = AAM::api()->posts();
foreach ($pages as $i => $page) {
if (in_array($page->type, array('post_type', 'custom'), true)) {
if ($service->is_hidden($page->object_id)) {
unset($pages[$i]);
}
}
}
}
return $pages;
}
/**
* After post SELECT query
*
* @param array $clauses
* @param WP_Query $wpQuery
*
* @return array
* @access private
*
* @version 7.0.0
*/
private function _posts_clauses_request($clauses, $wp_query)
{
static $executing = false;
if (!$wp_query->is_singular && !$executing) {
$executing = true;
$clauses['where'] .= apply_filters(
'aam_posts_where_clause_filter',
$this->_prepare_post_query($wp_query),
$wp_query
);
$executing = false;
}
return $clauses;
}
/**
* Modify content query to hide posts
*
* @param WP_Query $wp_query
*
* @return string
* @access private
*
* @version 7.0.0
*/
private function _prepare_post_query($wp_query)
{
global $wpdb;
if (!empty($wp_query->query['post_type'])) {
$post_type = $wp_query->query['post_type'];
} elseif (!empty($wp_query->query_vars['post_type'])) {
$post_type = $wp_query->query_vars['post_type'];
} elseif ($wp_query->is_attachment) {
$post_type = 'attachment';
} elseif ($wp_query->is_page) {
$post_type = 'page';
} else {
$post_type = 'any';
}
if ($post_type === 'any') {
$post_type = array_keys(get_post_types(array(), 'names'));
}
$area = AAM::api()->misc->get_current_area();
$post_types = (array) $post_type;
$not_in = [];
foreach (AAM::api()->posts()->aggregate() as $id => $perms) {
// Extracting post attributes
list($post_id, $post_type) = explode('|', $id);
// Extracting post LIST permission
$perm = isset($perms['list']) ? $perms['list'] : null;
if (is_array($perm)
&& (empty($perm['on']) || in_array($area, $perm['on'], true))
&& ($perm['effect'] !== 'allow')
&& in_array($post_type, $post_types, true)
) {
$not_in[] = $post_id;
}
}
if (!empty($not_in)) {
$query = " AND {$wpdb->posts}.ID NOT IN (" . implode(',', $not_in) . ")";
} else {
$query = '';
}
return $query;
}
/**
* Authorize RESTful action before it is dispatched by RESTful Server
*
* @param mixed $response
* @param object $request
*
* @return mixed
* @access private
*
* @version 7.0.3
*/
private function _rest_request_before_callbacks($response, $request)
{
// Register hooks that check post access
foreach (get_post_types(array('show_in_rest' => true)) as $type) {
add_filter(
"rest_prepare_{$type}", function($response, $post, $request) {
if ($request->get_param('context') !== 'edit') {
$response = $this->_authorize_post_rest_access(
$response, $post, $request
);
}
return $response;
}, 10, 3
);
}
// Override the password authentication handling ONLY for posts
$attrs = $request->get_attributes();
$callback = isset($attrs['callback']) ? $attrs['callback'] : null;
$controller = (is_array($callback) ? array_shift($callback) : null);
if (is_a($controller, 'WP_REST_Posts_Controller')) {
$post = get_post($request['id']);
$has_pass = isset($request['password']);
// Honor the manually defined password on the post
if (is_a($post, 'WP_Post')
&& empty($post->post_password)
&& $has_pass
&& ($request->get_method() === 'GET')
) {
$request['_password'] = $request['password'];
unset($request['password']);
}
}
return $response;
}
/**
* Check if post is allowed to be viewed through RESTful
*
* @param WP_REST_Response $response
* @param WP_Post $post
* @param WP_REST_Request $request
*
* @access public
* @return WP_REST_Response
*
* @version 7.0.3
*/
private function _authorize_post_rest_access($response, $post, $request)
{
$service = AAM::api()->posts();
if ($service->is_password_protected($post)) {
$password = isset($request['_password']) ? $request['_password'] : null;
if ($service->get_password($post) !== $password) {
$response->set_status(401);
$response->set_data([
'code' => 'rest_unauthorized',
'message' => 'The post is password protected. Invalid password provided.'
]);
}
} elseif ($service->is_redirected($post)) {
$redirect = $service->get_redirect($post);
// Determine redirect HTTP status code and use it if applicable for given
// redirect type
if (!empty($redirect['http_status_code'])) {
$status_code = $redirect['http_status_code'];
} else {
$status_code = 307;
}
$response->set_status($status_code);
$response->set_data([
'code' => 'rest_redirected',
'message' => 'The request is redirected to a different location',
'data' => [
'redirect_url' => AAM::api()->redirect->to_redirect_url(
$redirect
)
]
]);
} elseif ($service->is_restricted($post)) {
$response->set_status(401);
$response->set_data([
'code' => 'rest_unauthorized',
'message' => 'The content is restricted.'
]);
}
return $response;
}
/**
* Filter post content
*
* @param string $content
*
* @return string
* @access private
*
* @version 7.0.6
*/
private function _the_content($content)
{
static $in = false;
if (!$in) {
$in = true;
$post = AAM::api()->misc->get_current_post();
if (!empty($post)){
if (AAM::api()->posts()->is_teaser_message_set($post)) {
// Replace the [excerpt] placeholder with posts excerpt and do
// short-code evaluation
$content = do_shortcode(str_replace(
'[excerpt]',
$post->post_excerpt,
AAM::api()->posts()->get_teaser_message($post)
));
// Decorate message
if (AAM::api()->config->get('service.posts.decorate_teaser')) {
$content = apply_filters('the_content', $content);
}
}
}
$in = false;
}
return $content;
}
/**
* Check user capability
*
* This is a hack function that add additional layout on top of WordPress
* core functionality. Based on the capability passed in the $args array as
* "0" element, it performs additional check on user's capability to manage
* post, users etc.
*
* @param array $caps
* @param string $cap
* @param array $args
*
* @return array
* @access private
*
* @version 7.0.0
*/
private function _map_meta_cap($caps, $cap, $args)
{
global $post;
// For optimization reasons, check only caps that belong to registered post
// types
if (in_array($cap, $this->_content_capabilities, true)) {
// Critical part of the implementation. We do not know ahead what
// capability is responsible for what action when it comes to post types.
if (isset($args[0]) && is_scalar($args[0])) {
$post_id = intval($args[0]);
} elseif (is_a($post, 'WP_Post')) {
$post_id = $post->ID;
} else {
$post_id = null;
}
// If post_id is not empty, then, potentially we are checking
// permission to perform one of the action against a post
if (!empty($post_id)) {
if (is_a($post, WP_Post::class) && $post_id === $post->ID) {
$p = $post;
} else {
$p = get_post($post_id);
}
if (is_a($p, 'WP_Post')) {
$post_type = get_post_type_object($p->post_type);
if (is_a($post_type, 'WP_Post_Type')) {
$caps = $this->__map_post_type_caps(
$post_type,
$cap,
$caps,
$p,
$args
);
}
}
}
}
return $caps;
}
/**
* Map post type capability based on set permissions
*
* @param WP_Post_Type $post_type
* @param string $cap
* @param array $caps
* @param WP_Post $post
* @param array $args
*
* @return array
* @access private
*
* @version 7.0.10
*/
private function __map_post_type_caps(
WP_Post_Type $post_type,
$cap,
$caps,
WP_Post $post,
$args
) {
// Cover the scenario when $cap is not part of the post type capabilities
// There is a bug in the WP core when user is checked for 'publish_post'
// capability
$primitive_cap = array_search($cap, (array) $post_type->cap);
if ($primitive_cap === false) {
$primitive_cap = $cap;
}
switch ($primitive_cap) {
case 'edit_post':
case 'edit_posts':
case 'edit_others_posts':
case 'edit_private_posts':
case 'edit_published_posts':
// Cover the scenario when user uses Bulk Action or Quick Edit to
// change the Status to Published and post is not allowed to be
// published
$action = AAM::api()->misc->get($_SERVER, 'action');
$status = AAM::api()->misc->get($_SERVER, '_status');
if (
in_array($action, [ 'edit', 'inline-save'] , true)
&& $status === 'publish'
) {
$caps = $this->_map_publish_post_caps($caps, $post->ID);
} else {
$caps = $this->_map_edit_post_caps($caps, $post->ID);
}
break;
case 'delete_post':
case 'delete_posts':
case 'delete_private_posts':
case 'delete_published_posts':
case 'delete_others_posts':
$caps = $this->_map_delete_post_caps($caps, $post->ID);
break;
case 'read_post':
case 'read_private_posts':
$password = (isset($args[1]) ? $args[1] : null);
$caps = $this->_map_read_post_caps($caps, $post->ID, $password);
break;
case 'publish_post':
case 'publish_posts':
$caps = $this->_map_publish_post_caps($caps, $post->ID);
break;
default:
break;
}
return $caps;
}
/**
* Mutate capability meta map based on ability to publish the post
*
* @param array $caps
* @param int $post_id
*
* @return array
* @access private
*
* @version 7.0.0
*/
private function _map_publish_post_caps($caps, $post_id)
{
if (AAM::api()->posts()->is_denied_to($post_id, 'publish')) {
$caps[] = 'do_not_allow';
}
return $caps;
}
/**
* Mutate capability meta map based on ability to edit/update the post
*
* @param array $caps
* @param int $post_id
*
* @return array
* @access private
*
* @version 7.0.0
*/
private function _map_edit_post_caps($caps, $post_id)
{
$post = get_post($post_id);
$is_draft = $post->post_status === 'auto-draft';
if (!$is_draft && (AAM::api()->posts()->is_denied_to($post, 'edit'))) {
$caps[] = 'do_not_allow';
}
return $caps;
}
/**
* Mutate capability meta map based on ability to trash/delete the post
*
* @param array $caps
* @param int $post_id
*
* @return array
* @access private
*
* @version 7.0.0
*/
private function _map_delete_post_caps($caps, $post_id)
{
if (AAM::api()->posts()->is_denied_to($post_id, 'delete')) {
$caps[] = 'do_not_allow';
}
return $caps;
}
/**
* Mutate capability meta map based on ability to edit/update the post
*
* @param array $caps
* @param int $post_id
* @param string|null $password
*
* @return array
* @access private
*
* @version 7.0.2
*/
private function _map_read_post_caps($caps, $post_id, $password = null)
{
$service = AAM::api()->posts();
if ($service->is_password_protected($post_id)) {
if ($service->get_password($post_id) !== $password) {
$caps[] = 'do_not_allow';
}
} elseif ($service->is_restricted($post_id)) {
$caps[] = 'do_not_allow';
} elseif ($service->is_teaser_message_set($post_id)) {
$caps[] = 'do_not_allow';
}
return $caps;
}
}