diff --git a/capsman-enhanced.php b/capsman-enhanced.php index e20e42a0..72cbdeb3 100644 --- a/capsman-enhanced.php +++ b/capsman-enhanced.php @@ -3,7 +3,7 @@ * Plugin Name: Capability Manager Enhanced * Plugin URI: https://publishpress.com * Description: Manage WordPress role definitions, per-site or network-wide. Organizes post capabilities by post type and operation. - * Version: 1.8.1 + * Version: 1.8.2-beta * Author: PublishPress * Author URI: https://publishpress.com * Text Domain: capsman-enhanced @@ -23,12 +23,12 @@ * @copyright Copyright (C) 2009, 2010 Jordi Canals; modifications Copyright (C) 2019 PublishPress * @license GNU General Public License version 3 * @link https://publishpress.com - * @version 1.8 + * @version 1.8.2-beta */ if ( ! defined( 'CAPSMAN_VERSION' ) ) { - define( 'CAPSMAN_VERSION', '1.8.1' ); - define( 'CAPSMAN_ENH_VERSION', '1.8.1' ); + define( 'CAPSMAN_VERSION', '1.8.2-beta' ); + define( 'CAPSMAN_ENH_VERSION', '1.8.2-beta' ); } if ( cme_is_plugin_active( 'capsman.php' ) ) { diff --git a/includes/filters-wp_rest_workarounds.php b/includes/filters-wp_rest_workarounds.php index cde9d1ff..6e025256 100644 --- a/includes/filters-wp_rest_workarounds.php +++ b/includes/filters-wp_rest_workarounds.php @@ -11,16 +11,23 @@ */ class WP_REST_Workarounds { - var $post_id = 0; - var $is_posts_request = false; + private $post_id = 0; + private $is_posts_request = false; + private $is_view_method = false; + private $params = []; + private $skip_filtering = false; - function __construct() { + public function __construct() { add_filter('rest_pre_dispatch', [$this, 'fltRestPreDispatch'], 10, 3); add_filter('user_has_cap', [$this, 'fltPublishCapReplacement'], 5, 3); - add_filter('pre_post_status', [$this, 'fltPostStatus'], 10, 1); - } + add_filter('wp_insert_post_data', [$this, 'fltInsertPostData'], 10, 2); + add_filter('edit_post_status', [$this, 'fltPostStatus'], 10, 2); + add_filter('user_has_cap', [$this, 'fltRegulateUnpublish'], 5, 3); + add_action('admin_print_styles-post.php', [$this, 'actAdminPrintScripts']); + } + /** * Work around Gutenberg editor enforcing publish_posts capability instead of edit_published_posts. * @@ -28,7 +35,9 @@ function __construct() { * - The query pertains to a specific post * - The post type and its capabilities are defined and match the current publish capability requirement * - The post is already published with a public status, or scheduled - * + * + * Filter hook: 'user_has_cap' + * * @author Kevin Behrens * @link https://core.trac.wordpress.org/ticket/47443 * @link https://github.com/WordPress/gutenberg/issues/13342 @@ -38,8 +47,12 @@ function __construct() { */ public function fltPublishCapReplacement($wp_sitecaps, $reqd_caps, $args) { + if ($this->skip_filtering) { + return $wp_sitecaps; + } + if ($reqd_cap = reset($reqd_caps)) { - // slight compromise for perf: apply this workaround only when publish_posts capability for post type follows typical pattern (publish_*) + // slight compromise for perf: apply this workaround only when cap->publish_posts property for post type follows typical pattern (publish_*) if (0 === strpos($reqd_cap, 'publish_')) { if (!empty($wp_sitecaps[$reqd_cap])) { return $wp_sitecaps; @@ -68,48 +81,126 @@ public function fltPublishCapReplacement($wp_sitecaps, $reqd_caps, $args) } /** - * If the post is already published, prevent the workaround from allowing status to be changed via "Switch to Draft" (or by any other means). + * Work around WordPress allowing user who can "edit_published_posts" but not "publish_posts" to unpublish a post. * - * This will also prevent users with edit_published capability but not publish capability from unpublishing via Quick Edit. - * - * @param string $post_status New post status about to be saved + * This is hooked to the edit_post_status filter and also called internally from REST update_item capability check (for Gutenberg) + * and wp_insert_post_data (for Classic Editor and Quick Edit) + * + * Filter hook: 'edit_post_status' + * + * @author Kevin Behrens + * @param int $post_status Post status being set + * @param int $post_id ID of post being modified */ - function fltPostStatus($post_status) { + public function fltPostStatus($post_status, $post_id) { global $current_user; - - if ($_post = get_post($this->getPostID())) { - $type_obj = get_post_type_object($_post->post_type); - $status_obj = get_post_status_object($_post->post_status); - if ($type_obj && $status_obj && (!empty($status_obj->public) || !empty($status_obj->private) || 'future' == $_post->post_status)) { - if (empty($current_user->allcaps[$type_obj->cap->publish_posts])) { - $post_status = $_post->post_status; - } + $new_status_obj = get_post_status_object($post_status); + if (!$new_status_obj || !empty($new_status_obj->internal)) { + return $post_status; + } + + if (!$_post = get_post($post_id)) { + return $post_status; + } + + $type_obj = get_post_type_object($_post->post_type); + $status_obj = get_post_status_object($_post->post_status); + + if ($type_obj && $status_obj && (!empty($status_obj->public) || !empty($status_obj->private) || 'future' == $_post->post_status)) { + $this->skip_filtering = true; + + //if (empty($current_user->allcaps[$type_obj->cap->publish_posts])) { + if (!current_user_can($type_obj->cap->publish_posts)) { + $post_status = $_post->post_status; } + + $this->skip_filtering = false; } + return $post_status; } - private function getPostID() - { - global $post; - - if (defined('REST_REQUEST') && REST_REQUEST && $this->is_posts_request) { - return $this->post_id; - } + /** + * Regulate post unpublishing on Classic Editor and Quick Edit updates + * + * Filter hook: 'wp_insert_post_data' + * + * @param array $data Parsed array of Post data being set + * @param array $postarr ARray of current post data + */ + public function fltInsertPostData($data, $postarr) { + if (!empty($data['post_status']) && !empty($postarr['post_ID'])) { + $data['post_status'] = $this->fltPostStatus($data['post_status'], $postarr['post_ID']); + } - if (!empty($post) && is_object($post)) { - return ('auto-draft' == $post->post_status) ? 0 : $post->ID; - } elseif (isset($_REQUEST['post'])) { - return (int)$_REQUEST['post']; - } elseif (isset($_REQUEST['post_ID'])) { - return (int)$_REQUEST['post_ID']; - } elseif (isset($_REQUEST['post_id'])) { - return (int)$_REQUEST['post_id']; - } + return $data; } + + /** + * Regulate post unpublishing on Gutenberg "Switch to Draft" + * + * Filter hook: user_has_cap + * + * @param array $wp_sitecaps Array of user capabilities acknowledged for this request. + * @param array $reqd_caps Capability requirements + * @param array $args Additional arguments passed into user_has_cap filter + */ + public function fltRegulateUnpublish($wp_sitecaps, $reqd_caps, $args) + { + if (!defined('REST_REQUEST') || !REST_REQUEST || !$this->is_posts_request || !$this->post_id || $this->skip_filtering) { + return $wp_sitecaps; + } + if ($reqd_cap = reset($reqd_caps)) { + // slight compromise for perf: apply this workaround only when cap->edit_published_posts property for post type follows typical pattern (edit_published_*) + if (0 === strpos($reqd_cap, 'edit_published_')) { + if ($this->params && !empty($this->params['status'])) { + $set_status = $this->fltPostStatus($this->params['status'], $this->post_id); + if ($set_status != $this->params['status']) { + unset($wp_sitecaps[$reqd_cap]); + } + } + } + } + + return $wp_sitecaps; + } + + /** + * If we are blocking Gutenberg "Switch to Draft" by capability filtering, also hide the button + * + * Action hook: 'admin_print_styles-post.php' + */ + public function actAdminPrintScripts() { + global $current_user, $post; + + if (empty($post) || !did_action('enqueue_block_editor_assets')) { + return; + } + + $status_obj = get_post_status_object($post->post_status); + + if (!$status_obj || (empty($status_obj->public) && empty($status_obj->private))) { + return; + } + + $type_obj = get_post_type_object($post->post_type); + $this->skip_filtering = true; + + if ($type_obj && !current_user_can($type_obj->cap->publish_posts) && current_user_can($type_obj->cap->edit_published_posts)): ?> + + skip_filtering = false; + } + + /** + * Log REST query parameters for possible use by subsequent filters + * + * Action hook: 'rest_pre_dispatch' + */ public function fltRestPreDispatch($rest_response, $rest_server, $request) { $method = $request->get_method(); @@ -136,6 +227,9 @@ public function fltRestPreDispatch($rest_response, $rest_server, $request) } $this->is_posts_request = true; + $this->is_view_method = in_array($method, [\WP_REST_Server::READABLE, 'GET']); + $this->params = $request->get_params(); + break 2; } } @@ -143,4 +237,26 @@ public function fltRestPreDispatch($rest_response, $rest_server, $request) return $rest_response; } + + /** + * Determine the Post ID, if any, which this query pertains to + */ + private function getPostID() + { + global $post; + + if (defined('REST_REQUEST') && REST_REQUEST && $this->is_posts_request) { + return $this->post_id; + } + + if (!empty($post) && is_object($post)) { + return ('auto-draft' == $post->post_status) ? 0 : $post->ID; + } elseif (isset($_REQUEST['post'])) { + return (int)$_REQUEST['post']; + } elseif (isset($_REQUEST['post_ID'])) { + return (int)$_REQUEST['post_ID']; + } elseif (isset($_REQUEST['post_id'])) { + return (int)$_REQUEST['post_id']; + } + } } diff --git a/includes/filters.php b/includes/filters.php index dc6e1042..c9401cdb 100644 --- a/includes/filters.php +++ b/includes/filters.php @@ -26,6 +26,11 @@ function add( $object ) { $cme_extensions->add( new CME_WooCommerce() ); } +if (!defined('CME_DISABLE_WP_EDIT_PUBLISHED_WORKAROUND')) { + require_once (dirname(__FILE__) . '/filters-wp_rest_workarounds.php'); + new PublishPress\Capabilities\WP_REST_Workarounds(); +} + if ( is_admin() ) { global $pagenow; if ( 'edit.php' == $pagenow ) { diff --git a/readme.txt b/readme.txt index f7c0a635..b4497da1 100644 --- a/readme.txt +++ b/readme.txt @@ -68,6 +68,9 @@ Yes. Users with the 'manage_capabilities' capability can edit roles. This Capabi == Changelog == += 1.8.2-beta = + * Change : Reinstate WordPress edit_published_posts workaround with correct status filtering behavior + = 1.8.1 - 25 Oct 2019 = * Fixed : Autodraft publication, incorrect WooCommerce status storage (since 1.8)