Capabilities for custom post types in WordPress

I’ve just had to break out some serious Capabilities in WordPress today, and I’m documenting the process here in case it helps me in the future (or the past, though it seems unlikely) or anyone else.

The scenario i: I have a custom post type (Video) and I want to allow Editors and Admins to do whatever they want with it. Contributors and Authors (and below) shouldn’t be able to do anything with Video, and I want to create a separate Video Editor role which can do whatever it wants with Video posts. Video Editors should be able to upload images to use as Featured Image for a Video post, but not, as far as possible, mess with the other images in the site.

The work I’m doing breaks down into three sections:

  1. Set up the post type with a custom set of capabilities based on the post type name
  2. Assign the relevant capabilities to Editor and Admin roles, and create the new role of Video Editor
  3. Ensure the Video Editor can do what they need with attachments, but no more

The first part is pretty easy, you need to ensure that the full set of capabilities and mappable capabilities get baked into the plugin. Make sure you set the map_meta_cap, so you get all these caps (section of output from var_dump of the post type object):

  public 'cap' => 
    object(stdClass)[307]
      public 'edit_post' => string 'edit_video' (length=10)
      public 'read_post' => string 'read_video' (length=10)
      public 'delete_post' => string 'delete_video' (length=12)
      public 'edit_posts' => string 'edit_videos' (length=11)
      public 'edit_others_posts' => string 'edit_others_videos' (length=18)
      public 'publish_posts' => string 'publish_videos' (length=14)
      public 'read_private_posts' => string 'read_private_videos' (length=19)
      public 'read' => string 'read' (length=4)
      public 'delete_posts' => string 'delete_videos' (length=13)
      public 'delete_private_posts' => string 'delete_private_videos' (length=21)
      public 'delete_published_posts' => string 'delete_published_videos' (length=23)
      public 'delete_others_posts' => string 'delete_others_videos' (length=20)
      public 'edit_private_posts' => string 'edit_private_videos' (length=19)
      public 'edit_published_posts' => string 'edit_published_videos' (length=21)

If you don’t set map_meta_cap you will end up with just these caps:

  public 'cap' => 
    object(stdClass)[307]
      public 'edit_post' => string 'edit_video' (length=10)
      public 'read_post' => string 'read_video' (length=10)
      public 'delete_post' => string 'delete_video' (length=12)
      public 'edit_posts' => string 'edit_videos' (length=11)
      public 'edit_others_posts' => string 'edit_others_videos' (length=18)
      public 'publish_posts' => string 'publish_videos' (length=14)
      public 'read_private_posts' => string 'read_private_videos' (length=19)
      public 'read' => string 'read' (length=4)

That’s the post type setup done. Now to create the Video Editor role; here I made a quick array of the necessary capabilities (this comes in handy when adding them to the existing roles), then added the Video Editor role and passed in the video_caps array.

if ( ! isset( $GLOBALS[ 'wp_roles' ] ) )
	$GLOBALS[ 'wp_roles' ] = new WP_Roles();

$video_caps = array( 
	'delete_others_videos' => true,
	'delete_private_videos' => true,
	'delete_published_videos' => true,
	'delete_video' => true,
	'delete_videos' => true,
	'edit_others_videos' => true,
	'edit_private_videos' => true,
	'edit_published_videos' => true,
	'edit_video' => true,
	'edit_videos' => true,
	'publish_videos' => true,
	'read' => true,
	'read_private_videos' => true,
	'read_video' => true,
	'upload_files' => true,
);
// Add in Video Editor role
$video_editor_role = $GLOBALS[ 'wp_roles' ]->add_role( 'video_editor', __( 'Video Editor', 'cftp_band' ), $video_caps );

Now I can loop over the $video_caps for each of the Editor and Administrator role objects, using the add_cap method to, errrr, add the capability.

// Set editors and admins so they can do stuff with videos
$video_roles = array( 'editor', 'administrator' );
foreach ( $video_roles as $role_name ) {
	$role_object = get_role( $role_name );
	foreach ( $video_caps as $cap => $bool ) {
		// Check if the user has the equivalent post capability
		// and give them the video capability if they do.
		$equiv_cap = preg_replace( '/_video(s)?$/', '_post$1', $cap );
		if ( $role_object->has_cap( $equiv_cap ) )
			$role_object->add_cap( $cap );
	}
}

Note that adding a capability to a role and adding a whole new role only needs to be done once, then it’s stored by WordPress. You do not need to run these routines on every page load. This also goes for plugins which manipulate the roles and capabilities, they can very often be activated, used to change the roles/capabilities, then de-activated and the effects will persist.

The last problem is an “interesting” one: I want the Video Editor to be able to upload images through the post to use as the Featured Image, I’m even happy for them to be able to use existing images as a featured images. I do not want them to be able to edit (or delete) images not related to Video posts.

I’ve taken a two fold approach to this: first I’ve used the map_meta_cap filter to check if the attachment they are deleting has a post_parent which is a Video and secondly whether they can edit that Video. Here’s the code for that:

/**
 * Hooks the WP map_meta_cap filter.
 *
 * @param array $caps An array of capabilities that the user must have to be allowed the requested capability
 * @param array $cap The specific capability requested
 * @param int $user_id The ID of the user whose capability we are checking
 * @param array $args The arguments passed when checking for the capability
 * @return array An array of capabilities that the user must have to be allowed the requested capability
 **/
function my_map_meta_cap( $caps, $cap, $user_id, $args ) {
	// We're going to use map_meta_cap to check for the ability to edit the
	// parent post of the attachment. If the user can edit the parent post,
	// we will allow them to edit this attachment. This should cover scenarios where
	// images are uploaded to become a featured image for a video.
	if ( 'edit_post' == $cap || 'delete_post' == $cap ) {
		$attachment = get_post( $args[ 0 ] );
		if ( 'attachment' == $attachment->post_type ) {
			$parent = get_post( $attachment->post_parent );
			if ( 'video' == $parent->post_type && user_can( $user_id, 'edit_post', $parent->ID ) ) {
				return array( 'edit_videos' );
			}
		}
	}
	return $caps;
}
add_filter( 'map_meta_cap','my_map_meta_cap, null, 4 );

Then finally, in a flurry of hacky code, I force a wp_die on the Media pages like so and then remove the link to them if the user can’t edit_posts:

function my_load_media() {
	if ( !current_user_can( 'edit_posts' ) )
		wp_die( __( 'You do not have permission to access the Media Library.', 'ird' ) );
}
add_action( 'load-media-new.php', 'my_load_media' );
add_action( 'load-media.php', 'my_load_media' );
add_action( 'load-upload.php', 'my_load_media' );

function admin_menu() {
	if ( ! current_user_can( 'edit_posts' ) )
		remove_menu_page( 'upload.php' );
}
add_action( 'admin_menu','my_admin_menu, 11 )

This last code block is essentially cribbed from John Blackbourn with thanks. :)

…and that’s that. Hope it helps.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.