Categories
Past Tutorials WordPress

Trying To Understand WordPress’s Capabilities

I recently finished a WordPress project that needed a “podcast” custom post type. The post type required restricted use to one user role, however, I noticed that the custom post type was available to all user roles. I then changed the capability_type argument to “podcast”, and the custom post type disappeared completely. 

It dawned on me that for as long as I’ve been developing with WordPress, I didn’t quite fully understand the relationship between custom post type capabilities and user role capabilities. And while Justin Tadlock’s post on Meta Capabilities is required reading, it didn’t quite cover everything to demystify the relationship between custom post type meta capabilities and user roles.

The Typical Scenario

You want to create a custom post type, that only a particular user role can use.

In this example, I am going to create a “Secrets” custom post type and I want only Administrators to have access to it.

add_action( 'init', 'codex_secret_init' );

/**
 * Register a Secret post type.
 *
 * @link http://codex.wordpress.org/Function_Reference/register_post_type
 */
function codex_secret_init() {
	$labels = array(
		'name'               => _x( 'Secrets', 'post type general name', 'your-plugin-textdomain' ),
		'singular_name'      => _x( 'Secret', 'post type singular name', 'your-plugin-textdomain' ),
		'menu_name'          => _x( 'Secrets', 'admin menu', 'your-plugin-textdomain' ),
		'name_admin_bar'     => _x( 'Secret', 'add new on admin bar', 'your-plugin-textdomain' ),
		'add_new'            => _x( 'Add New', 'Secret', 'your-plugin-textdomain' ),
		'add_new_item'       => __( 'Add New Secret', 'your-plugin-textdomain' ),
		'new_item'           => __( 'New Secret', 'your-plugin-textdomain' ),
		'edit_item'          => __( 'Edit Secret', 'your-plugin-textdomain' ),
		'view_item'          => __( 'View Secret', 'your-plugin-textdomain' ),
		'all_items'          => __( 'All Secrets', 'your-plugin-textdomain' ),
		'search_items'       => __( 'Search Secrets', 'your-plugin-textdomain' ),
		'parent_item_colon'  => __( 'Parent Secrets:', 'your-plugin-textdomain' ),
		'not_found'          => __( 'No Secrets found.', 'your-plugin-textdomain' ),
		'not_found_in_trash' => __( 'No Secrets found in Trash.', 'your-plugin-textdomain' )
	);

	$args = array(
		'labels'             => $labels,
        'description'        => __( 'Description.', 'your-plugin-textdomain' ),
		'public'             => true,
		'publicly_queryable' => true,
		'show_ui'            => true,
		'show_in_menu'       => true,
		'query_var'          => true,
		'rewrite'            => array( 'slug' => 'secret' ),
		'capability_type'    => 'post',
		'map_meta_cap' => true,
		'has_archive'        => true,
		'hierarchical'       => false,
		'menu_position'      => null,
		'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' )
	);

	register_post_type( 'secrets', $args );
}

…and it shows up in the backend as we expect, when we’re logged in as an administrator…

..however, if we log in as anything between a Contributer and editor, our secrets are not safe…

…so we read the documentation, and assume that if we change the capability_type argument to:

'capability_type' => array( 'secret','secrets' ),

However we assumed incorrectly, for our secret is no longer visible to anyone, including administrators:

The popular solution to this issue, (and it’s not an issue as we will learn later) that you will encounter on stackoverflow or wordpress exchange is to simply change capability_type back to post and apply hacks to remove it from the dashboard menu for other user roles. THAT IS INCORRECT.

Creating and Mapping the Capabilities

Per the  register_post_type codex, we need to set and map the meta capabilities and primitive capabilities of our capability type:

function codex_secret_init() {
	$labels = array(
		'name'               => _x( 'Secrets', 'post type general name', 'your-plugin-textdomain' ),
		'singular_name'      => _x( 'Secret', 'post type singular name', 'your-plugin-textdomain' ),
		'menu_name'          => _x( 'Secrets', 'admin menu', 'your-plugin-textdomain' ),
		'name_admin_bar'     => _x( 'Secret', 'add new on admin bar', 'your-plugin-textdomain' ),
		'add_new'            => _x( 'Add New', 'Secret', 'your-plugin-textdomain' ),
		'add_new_item'       => __( 'Add New Secret', 'your-plugin-textdomain' ),
		'new_item'           => __( 'New Secret', 'your-plugin-textdomain' ),
		'edit_item'          => __( 'Edit Secret', 'your-plugin-textdomain' ),
		'view_item'          => __( 'View Secret', 'your-plugin-textdomain' ),
		'all_items'          => __( 'All Secrets', 'your-plugin-textdomain' ),
		'search_items'       => __( 'Search Secrets', 'your-plugin-textdomain' ),
		'parent_item_colon'  => __( 'Parent Secrets:', 'your-plugin-textdomain' ),
		'not_found'          => __( 'No Secrets found.', 'your-plugin-textdomain' ),
		'not_found_in_trash' => __( 'No Secrets found in Trash.', 'your-plugin-textdomain' )
	);

	$args = array(
		'labels'             => $labels,
        'description'        => __( 'Description.', 'your-plugin-textdomain' ),
		'public'             => true,
		'publicly_queryable' => true,
		'show_ui'            => true,
		'show_in_menu'       => true,
		'query_var'          => true,
		'rewrite'            => array( 'slug' => 'secret' ),
		'capability_type'    => array( 'secret','secrets' ),
		'capabilities' => array(
			'publish_posts' => 'publish_secrets',
			
			'read' => 'read_secret',
			'read_private_posts' => 'read_private_secrets',
			
			'edit_post' => 'edit_secret',
			'edit_posts' => 'edit_secrets',
			'edit_others_posts' => 'edit_others_secrets',
			'edit_private_posts' => 'edit_private_secrets',
			'edit_published_posts' => 'edit_published_secrets',
			
			'delete_post' => 'delete_secret',
			'delete_posts' => 'delete_secrets',
			'delete_others_posts' => 'delete_others_secrets',
			'delete_private_posts' => 'delete_private_secrets',
			'delete_published_posts' => 'delete_published_secrets',
		),
		'map_meta_cap' => true,
		'has_archive'        => true,
		'hierarchical'       => false,
		'menu_position'      => null,
		'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' )
	);

	register_post_type( 'secrets', $args );
}

Note that the “map_meta_cap” argument must be set to true. Re-saving the permalinks will trigger the flush_rewrite_rules function. However you will notice that our secrets are still hidden from us.

Since Justin Tadlock has a great write up on mapping meta capabilities to user capabilities, I won’t repeat the tutorial.

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

function my_secret_map_meta_cap( $caps, $cap, $user_id, $args ) {
	
	/* If editing, deleting, or reading a secret, get the post and post type object. */
		if ( 'edit_secret' == $cap || 'delete_secret' == $cap || 'read_secret' == $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 ( 'edit_secret' == $cap || 'delete_secret' == $cap || 'read_secret' == $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 secret, assign the required capability. */
	if ( 'edit_secret' == $cap ) {
		if ( $user_id == $post->post_author )
			$caps[] = $post_type->cap->edit_posts;
		else
			$caps[] = $post_type->cap->edit_others_posts;
	}

	/* If deleting a secret, assign the required capability. */
	elseif ( 'delete_secret' == $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 secret, assign the required capability. */
	elseif ( 'read_secret' == $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;
}

If you’re following along, after adding the mapped secret capabilities, you’ll realize that we STILL can’t see our secrets. Justin’s tutorial recommended the “Members Plugin” to finish off the process, and while that’s an awesome plugin that I highly recommend, there’s still too much magic going on behind the scenes for us to thoroughly understand what’s happening.

Applying the Mapped Capabilities to User Roles

Drum role…

add_action( 'admin_init', 'add_secret_caps');

function add_secret_caps() {
    // gets the desired user role
    $role = get_role( 'administrator' );

    // Set role capabilities

	$role->add_cap('publish_secrets');
	
	$role->add_cap('read_secret');
	$role->add_cap('read_private_secrets');
	
	$role->add_cap('edit_secret');
	$role->add_cap('edit_secrets');
	$role->add_cap('edit_others_secrets');
	$role->add_cap('edit_private_secrets');
	$role->add_cap('edit_published_secrets');
	
	$role->add_cap('delete_secret');
	$role->add_cap('delete_secrets');
	$role->add_cap('delete_others_secrets');
	$role->add_cap('delete_private_secrets');
	$role->add_cap('delete_published_secrets');
}

NOW if you refresh, you’ll see your secrets, for your eyes only, for if you switch to any user between Contributor and Editor, your secrets are hidden.

The Members plugin handles the addition and removal of capabilities elegantly, and I would recommend using the plugin over adding them via a hook, however, it is important that you understand how to manually add meta capabilities to user roles if you feel that a plugin is overkill for your project.

So What Are Some Of  The Capabilities?

Let’s say you only wanted to share your secrets with other administrators, but you wanted to post them on the home page…

add_action( 'pre_get_posts', 'add_secrets_to_home' );

function add_secrets_to_home( $query ) {
	
	//We're not going to use the "current_user_can" because it will throw an error in this use case
	$current_user = wp_get_current_user(); 

	$query->set( 'post_type', array( 'post') );
	
    if( $query->is_main_query() && $query->is_home() ) {
	    if( !empty($current_user->allcaps['read_secret']) && $current_user->allcaps['read_secret']){
		    $query->set( 'post_type', array( 'post', 'secrets') );
	    }
    }
}

I hope this tutorial clarified some of the more challenging aspects of custom post types and user roles. Please leave a comment below for any suggestions or corrections.