7

I want to add a couple of editors that can edit_others_posts, but I do not want them to be able to publish others posts, only save the post by clicking on the Submit for Review button.

How can I do this?

EDIT: To explain this in detail. Currently, we can't allow a user to edit others post by only saving the post. If edit_others_post is enabled for the user, then they can publish the post.

Workflow I'm aiming for:

  1. Editors can only edit others pending posts (solved here).
  2. Editors can save the pending post but not publish it. So the Submit for Review button will be available for them (which is the "update post" button when a post is in pending mode)
  • So you want to prevent users of a certain role from being able to change a posts status? – Tom J Nowell Sep 11 '14 at 15:11
  • @TomJNowell See my updated question with further details. – Christine Cooper Sep 11 '14 at 15:28
  • Have you tried the post status switch actions? (search core) – kaiser Sep 11 '14 at 17:50
  • Not heard about this thus far. How does it work in this respects? – Christine Cooper Sep 12 '14 at 13:28
  • Example question about post transition hook. – kaiser Sep 12 '14 at 20:44
8
+50

If I understand well, user having a special role in your site should:

  • Be able to edit own posts in all statuses but 'publish' and not be able to publish them, just send for revision
  • Be able to edit others posts only when pending, but not be able to publish them, just send for revision
  • Never be able to delete other posts, no matter the status

If so, it seems to me a role more similar to an 'author' then an 'editor'.

Only differences with author is that

  • user of your role can't edit published posts, even if they are the author
  • user of your role can edit others pending posts but not publishing them

So first suggestion I can give is create a custom role, using role 'author' as strting point, removing the 3 unwanted caps and adding the custom one, a simple class that do that:

class CustomEditorRole {

  private static $role = 'authorplus';
  private $role_label;

  function __construct() {
    // here we need a real, loaded, text domain
    $this->role_label = __( 'Author +', 'yout-txt-dmn' );
  }

  function addRole() {
    global $wp_roles;
    if ( ! $wp_roles instanceof WP_Roles ) {
      $wp_roles = new WP_Roles;
    }
    $author = get_role( 'author' ); 
    $caps = $author->capabilities; // start with author capabilities
    $caps['publish_posts'] = FALSE;
    $caps['edit_published_posts'] = FALSE;
    $caps['delete_published_posts'] = FALSE;
    $caps['edit_others_pending_posts'] = TRUE; // custom cap
    // create new role with custom caps
    add_role( self::$role, $this->role_label, $caps );
  }

  function removeRole() {
    global $wp_roles;
    if ( ! $wp_roles instanceof WP_Roles ) {
      $wp_roles = new WP_Roles;
    }
    remove_role(self::$role);
  }

}

Let's add the action on plugin activation / deactivation:

register_activation_hook( __FILE__, array( new CustomEditorRole, 'addRole' ) );
register_deactivation_hook( __FILE__, array( new CustomEditorRole, 'removeRole' ) );

Here I assume previous code is in main plugin file.

Capabilities we set above are valid for every post, no matter post author or post status, Now we have to allow user with our custom role to edit others post when pending.

First problem we encounter is that on post list screen (edit.php), if the capability edit_others_posts in not enabled for the user (and for our custom role it isn't), then posts by other users are not shown in the list, because stripped out from the query and when query happen we have no access on posts data, so we have to just assign the capability, mo matter post status, at least until the query runs.

Second problem is that, on saving, before give to user with custom roles the edit_others_posts cap, we have to check not only that current status is "pending" but also that user is not trying to change it. That can be done looking at informations in $_POST data. It means we need 2 "routines", one that runs on admin screens (edit.php and post.php) the second that run during post saving.

The way to give to our custom role user the edit_others_post capability only for pending posts is add a filter to 'user_has_cap'.

Inside the filter callback we can implement this workflow:

  1. check if the capability to filter is one the 2 we want to manage ('edit-post' or 'edit-others-posts', check if we are in admin, check if the user has our custom capability and it is not an editor or an admin. If all that conditions are true we can go on, otherwise we have to do nothing, i.e. return original capabilities
  2. check if we are saving or not, and run 2 different routines:
    • Routine when saving
    • Routine when not saving

Routine when saving:

  1. check that the current action is edit post
  2. get the post information from $_POST data, check if the post has the right post type and is pending
  3. check that pending status can be changed only by an admin or a "real" editor
  4. if all previous checks pass, assign to user the 'edit-others-posts' capability ('edit-post' will be mapped automatically)

Routine when not saving:

  1. Check that we are in one of the 2 screens of interest, if not, do nothing
  2. different behaviour depending on capability to filter:
    • when capability to filter is 'edit-others-posts' we have no post data, so just assign it but only before main query not already happen and only on edit.php screen
    • when capability to filter is 'edit-post' get post data and if post is pending assign to user the 'edit-others-posts' cap ('edit-post' will be mapped automatically)

There is last thing to do. Using described workflow custom role users will not be able to preview others pending posts, even if they are able to edit them.

We can filter again capability, but there's a simpler way: during main query (using one of the dozen of hooks fired by WP_Query) we can just take the $wp_post_statuses['pending'] object and set its public property to true when current user has our custom role: the only effect is that pending posts are previewable and once we don't change any capability we can stay safe.

Ok, just translate words in code:

class CustomEditorCaps {

  function manageCaps( $allcaps, $caps, $args, $user ) {    
    if ( ! $this->shouldManage( $args[0], $user ) ) {
      return $allcaps;
    }
    // Are we saving?
    $action = filter_input( INPUT_POST, 'action', FILTER_SANITIZE_STRING );
    $method = strtoupper(filter_var($_SERVER['REQUEST_METHOD'], FILTER_SANITIZE_STRING ));
    if ( $method !== 'POST' ) { // not saving
      global $pagenow;
      // we are interested only on post list and  post edit screens
      if (
        is_admin()
        && in_array( $pagenow, array( 'post.php', 'post-new.php', 'edit.php' ), TRUE
      ) ) {
        $screen_id = $pagenow === 'edit.php' ? 'edit-post' : 'post';
        $allcaps = $this->maybeAllow( $args, $allcaps, $user, $screen_id );
      }
    } elseif ( $action === 'editpost' ) { // saving and right action
      $allcaps = $this->maybeAllowOnSave( $args, $allcaps, $user  );
    }
    return $allcaps; // always return: it's a filter
  }

  function lockPendingStatus( $data, $postarr ) {
    if (
       isset( $postarr['ID'] )
       && ! empty($postarr['ID'])
       && $data['post_type'] === 'post' // 'post' post type
       && $data['post_status'] !== 'pending' // a non pending status
       && ! current_user_can( 'delete_others_posts' ) // current user is not an admin
    ) {
       $orig = get_post_status( $postarr['ID'] ); 
       if ( $orig === 'pending' ) { // hey post was pending!
          $data['post_status'] = 'pending'; // let's restore pending status
       }
    }
    return $data; // always return: it's a filter
  }

  function allowPreview( $posts, $query ) {
    if ( is_admin()
      || ! $query->is_main_query()
      || empty( $posts )
      || ! $query->is_single
      || $posts[0]->post_type !== 'post'
    ) {
      return $posts; // return first argument: it's a filter
    }
    $status = get_post_status( $posts[0] );
    $post_status_obj = get_post_status_object( $status );
    if (
      ! $post_status_obj->public
      && $status === 'pending'
      && current_user_can('edit_others_pending_posts')
    ) {
      // post is pending and our user has our special role
      // allow preview
      global $wp_post_statuses;
      $wp_post_statuses[$status]->public = TRUE;
    }
    return $posts; // return first argument: it's a filter
  }

  private function maybeAllow( $args, $allcaps, $user, $screen ) {
    if ( $args[0] === 'edit_others_posts' ) {
      // if filtering 'edit_others_posts' we have no access to single post data
      // allow cap only on post list screen and before querying posts
      $allcaps['edit_others_posts'] = ! did_action('pre_get_posts')
        && $screen === 'edit-post';
      return $allcaps;
    }
    $post = get_post( $args[2] );
    if (  $post->post_status === 'pending' ) {
      $allcaps['edit_others_posts'] = TRUE;
    }
    return $allcaps; // always return: it's a filter
  }

  private function maybeAllowOnSave( $args, $allcaps, $user ) {
    $data = $this->getPostedData();
    if ( $data['post_type'] !== 'post' || (int) $data['post_ID'] <= 0 ) {
      return $allcaps;
    }
    $post = get_post( $data['post_ID'] );
    if (
      $post->post_status === 'pending'
      && $data['original_post_status'] === 'pending'
      && ( empty( $data['post_status'] ) || $data['post_status'] === 'pending' )
    ) {
      // if post is pending and will stay pending allow editing
      $allcaps['edit_others_posts'] = true;
    }
    return $allcaps;
  }

  private function shouldManage( $cap, $user ) {
    return is_admin() // not affect frontend
      && in_array( $cap, array( 'edit_others_posts', 'edit_post' ), TRUE )
      && ! $user->has_cap( 'delete_others_posts' ) // real editor or more
      && $user->has_cap( 'edit_others_pending_posts' ) // our role
      && ! defined( 'DOING_AJAX' ); // does not affect ajax
  }

  private function getPostedData() {
    return filter_input_array( INPUT_POST, array(
      'post_type'            => FILTER_SANITIZE_STRING,
      'post_ID'              => FILTER_SANITIZE_NUMBER_INT,
      'original_post_status' => FILTER_SANITIZE_STRING,
      'post_status'          => FILTER_SANITIZE_STRING,
    ) );
  }

}

And add the 2 relevant hooks: one for filtering 'user_has_cap', one to ensure pending status can be changed only by admins or real editors and the last filtering 'posts_results' to allow preview:

$cap_manager = new CustomEditorCaps;
add_filter( 'user_has_cap', array( $cap_manager, 'manageCaps' ), PHP_INT_MAX, 4 );
add_filter( 'posts_results', array( $cap_manager, 'allowPreview' ), 10, 2 );
add_filter( 'wp_insert_post_data', array( $cap_manager, 'lockPendingStatus' ), 10, 2 );

Once you have all this code in a plugin and you activate it, you have only to assign to users the custom role plugin creates.


All code available as plugin, in a Gist here.

  • 1
    +gmazzap that gist is no longer available – mawalker Mar 6 '18 at 11:45
  • 1
    @mawalker updated the Gist link, even if that's not some code I would be proud of today... – gmazzap Mar 7 '18 at 7:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.