Skip to content

Commit

Permalink
Reinstate edit_published_posts support, without post_status chaos
Browse files Browse the repository at this point in the history
The previous workaround for the WP "edit_published_posts without publish_posts" issue caused erreneous post_status storage.  This was due to filtering the 'pre_post_status' value based on a post_id derived from the request parameters.   The result was that the published status of the primary post was "restored" to the post being updated -  which may have been an autodraft, WooCommerce custom types or other supplemental posts updated within the request.  This is no longer possible because the post_status value is only filtered based on the post_id passed directly into the 'edit_post_status' or 'wp_insert_post_data' filter.

Post ID is still derived from the query, but only for the purpose publish_posts / edit_publish_posts capability checks.

This will be released as a beta version initially. As an ongoing precaution, the entire edit_published_posts workaround can be disabled by defining the following constant:

define('CME_DISABLE_WP_EDIT_PUBLISHED_WORKAROUND', true);
  • Loading branch information
agapetry committed Oct 26, 2019
1 parent f7729f8 commit daa102e
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 40 deletions.
8 changes: 4 additions & 4 deletions capsman-enhanced.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' ) ) {
Expand Down
188 changes: 152 additions & 36 deletions includes/filters-wp_rest_workarounds.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,33 @@
*/
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.
*
* Allow edit_published capability to satisfy publish capability requirement if:
* - 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
Expand All @@ -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;
Expand Down Expand Up @@ -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)): ?>
<style type="text/css">button.editor-post-switch-to-draft {display:none;}</style>
<?php endif;

$this->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();
Expand All @@ -136,11 +227,36 @@ 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;
}
}
}

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'];
}
}
}
5 changes: 5 additions & 0 deletions includes/filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down
3 changes: 3 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit daa102e

Please sign in to comment.