Meta capabilities for custom post types

While I was fiddling around with a new plugin that uses custom post types for WordPress the other day, I ran into a small issue that I hadn’t noticed before. Meta capabilities for custom post types were not being automatically mapped, so I couldn’t have granular control over permissions.

At first, I was a bit disappointed that this wasn’t taken care of because this would be a major blow to my plugin. However, I soon learned this could be considered a great feature rather than a bug.

Andrew Nacin mentioned that we’d need to “roll our own handling” for capabilities using the map_meta_cap hook. This hook gives you control over the meta capabilities as well as the power to step outside of the WordPress way of doing things. The more I play around with it, the more I’d rather WordPress not automatically map these.

Before reading on, please note that this is only an issue/feature if you're using custom capabilities for your post types. If not, this might not interest you much.

What are meta capabilities?

Meta capabilities are capabilities a user is granted on a per-post basis. The three we’re dealing with here are:

  • edit_post
  • delete_post
  • read_post

For regular blog posts, WordPress “maps” these to specific capabilities granted to user roles. For example, a user might be given the edit_post capability for post 100 if they are the post author or have the edit_others_posts capability.

When using custom post types, this mapping doesn’t happen automatically if you’re setting up custom capabilities. We have to come up with our own solution.

In this tutorial, I’ll be using the movie post type, so the above three meta capabilities for our purposes will be:

  • edit_movie
  • delete_movie
  • read_movie

Setting up your custom post type

At this point, I’ll assume you’re familiar with creating custom post types. If not, you’ll want to read through my detailed tutorial on them. The code presented below will be an abridged version of what you might use.

We’re going to focus on two arguments, capability_type and capabilities. These control the permissions for what a user with a specific role can do with the post type.

As mentioned earlier, the post type we’re going with is movie. So, let’s set up that post type:

add_action( 'init', 'create_my_post_types' );

function create_my_post_types() {
	register_post_type(
		'movie',
		array(
			'public' => true,
			'capability_type' => 'movie',
			'capabilities' => array(
				'publish_posts' => 'publish_movies',
				'edit_posts' => 'edit_movies',
				'edit_others_posts' => 'edit_others_movies',
				'delete_posts' => 'delete_movies',
				'delete_others_posts' => 'delete_others_movies',
				'read_private_posts' => 'read_private_movies',
				'edit_post' => 'edit_movie',
				'delete_post' => 'delete_movie',
				'read_post' => 'read_movie',
			),
		)
	);
}

I’ve also added in a couple of extra capabilities that were not shown in my previous tutorial on post types: delete_posts and delete_others_posts. We’re going to use these to give people the ability to delete posts (movies).

Assigning capabilities to roles

To assign specific capabilities to roles, you’re going to need a role management plugin. A good one to use is my Members plugin. You could code this if you wanted, but it’s a bit outside the scope of this tutorial.

What you need to do at this point is assign capabilities to the roles that should have them. Just be careful not to give too much power to the wrong roles.

  • publish_movies: This allows a user to publish a movie.
  • edit_movies: Allows editing of the user's own movies but does not grant publishing permission.
  • edit_others_movies: Allows the user to edit everyone else's movies but not publish.
  • delete_movies: Grants the ability to delete movies written by that user but not others' movies.
  • delete_others_movies: Capability to edit movies written by other users.
  • read_private_movies: Allows users to read private movies.
  • edit_movie: Meta capability assigned by WordPress. Do not give to any role.
  • delete_movie: Meta capability assigned by WordPress. Do not give to any role.
  • read_movie: Meta capability assigned by WordPress. Do not give to any role.

Just to say it again: it is important that you don’t assign those last three capabilities to any role. These are our meta capabilities that we’ll be mapping in the next section.

Mapping the meta capabilities

This part is completely customizable. What we’re doing here is filtering the map_meta_cap hook so we can do our own mapping. What this means is that users will be granted meta capabilities on a per-post basis so they can do things like edit their own posts.

I’m going to keep it extremely simple and follow my notes from the previous section. If you wanted, you could do all kinds of crazy things here.

You can simply drop the below in your plugin or theme’s functions.php (whichever you’re using) and be done with it. It’ll follow the rules I set forth above.

add_filter( 'map_meta_cap', 'my_map_meta_cap', 10, 4 );

function my_map_meta_cap( $caps, $cap, $user_id, $args ) {

	/* If editing, deleting, or reading a movie, get the post and post type object. */
	if ( 'edit_movie' == $cap || 'delete_movie' == $cap || 'read_movie' == $cap ) {
		$post = get_post( $args[0] );
		$post_type = get_post_type_object( $post->post_type );

		/* Set an empty array for the caps. */
		$caps = array();
	}

	/* If editing a movie, assign the required capability. */
	if ( 'edit_movie' == $cap ) {
		if ( $user_id == $post->post_author )
			$caps[] = $post_type->cap->edit_posts;
		else
			$caps[] = $post_type->cap->edit_others_posts;
	}

	/* If deleting a movie, assign the required capability. */
	elseif ( 'delete_movie' == $cap ) {
		if ( $user_id == $post->post_author )
			$caps[] = $post_type->cap->delete_posts;
		else
			$caps[] = $post_type->cap->delete_others_posts;
	}

	/* If reading a private movie, assign the required capability. */
	elseif ( 'read_movie' == $cap ) {

		if ( 'private' != $post->post_status )
			$caps[] = 'read';
		elseif ( $user_id == $post->post_author )
			$caps[] = 'read';
		else
			$caps[] = $post_type->cap->read_private_posts;
	}

	/* Return the capabilities required by the user. */
	return $caps;
}

Now taking questions

I realize there’s still quite a few things that I might not have covered in my tutorials on post types so far. Some people have probably figured these out by now, but others of you may be stuck.

Feel free to ask any questions you have about post types, and I’ll see about answering them or writing more tutorials based on your suggestions.