A guide to capabilities and custom post types

A guide to capabilities and custom post types

Sorting out capabilities for custom post types is tricky. At least I think it is – mainly because it’s hard to figure out what you’re supposed to be doing. Both the code and the documentation seem somewhat opaque to me.

So this is my attempt at unraveling how it works – together with some different ways you might want to set up your custom post types. And it is an attempt, so if I’ve got something wrong or I’m not making sense, please let me know…

Before delving into set up of custom post types though, there’s a fair bit of background we have to go through for the final bit to make any sense at all. Here’s what I’ll be covering:

  • How capabilities are used in WordPress – current_user_can.
  • Primitive and meta capabilities.
  • How contextual / meta capabilities work under the hood – map_meta_cap.
  • How WordPress checks custom post type capabilities.
  • Setting capabilities for customs post types.

How capabilities are used in WordPress – current_user_can

Capabilities are assigned to roles (and sometimes to users directly). They can then be checked by the function current_user_can() e.g., current_user_can( ‘edit_others_posts’ ). If you do a search for current_user_can in WordPress core, you’ll find it being used all over the place. We need to get our heads round how it works.

Primitive and meta capabilities

The codex entry on get_post_type_capabilities() explains that there are two types of capabilities – ‘primitive’ and ‘meta’ capabilities. I don’t think the nomenclature is very helpful, and I prefer to think of them as ‘generic’ capabilities (primitive) and ‘contextual’ capabilities (meta).

If we take the standard post type of ‘post’ as an example, it has the following capabilities:

// Contextual / meta capabilities:
edit_post
read_post
delete_post
// Generic / primitive capabilities:
edit_posts
edit_others_posts
publish_posts
read_private_posts
read
delete_posts
delete_private_posts
delete_published_posts
delete_others_posts
edit_private_posts
edit_published_posts
edit_posts

The first thing to understand is that the generic capabilities are generally plural (e.g., edit_posts) while the contextual capabilities are all singular (e.g., edit_post).

And the second thing to understand is that current_user_can works in different ways depending on which type of capability is being tested. Let’s take each in turn.

Checking a generic / primitive capability with current_user_can

This one’s simple. Current_user_can simply checks whether the user has that generic capability or not. So for example, current_user_can( ‘edit_posts’ ) returns true if the current user has the capability ‘edit_posts’.

Checking a contextual / meta capability with current_user_can

This is much more complex.

First, when checking a contextual / meta capability, current_user_can must take two arguments – both the capability and the ID of the object in question. For example, current_user_can(‘edit_post’) is incomplete – it needs to be current_user_can(‘edit_post’, $post_id). Now the singular and plural difference makes sense. When checking a contextual / meta capability you are always checking for a single specified object. When checking a generic / primitive capability (e.g., ‘edit_posts’) you are checking whether the user has the authority to edit any post of that post type.

Second, current_user_can doesn’t run a straightforward “Does the user have this capability?” check. Instead it may run more than one check depending on the context. For example, let’s say the post with ID 357 has been published. current_user_can(‘edit_post’, 357) will return true if:

  1. The user has the capability ‘edit_published_posts’; AND
  2. If the user is not the author of the post, the user must also have the capability of ‘edit_others_posts’.

In other words, in this case the relevant context is a) the post status and b) whether or not the user is the post author.

Different context means different checks. Let’s say the author of post 357 sets the post visibility to ‘private’ and current_user_can(‘edit_post’, 357) is run again. This time if the current user isn’t the post author then ‘edit_other_posts’ and ‘edit_private_posts’ are required – the requirement for ‘edit_published_posts’ has been replaced by ‘edit_private_posts’.

How contextual / meta capabilities work under the hood – map_meta_cap()

The contextual checking is handled by the function map_meta_cap which is called from within current_user_can. Put simply map_meta_cap takes the two arguments of current_user_can (the contextual capability to be checked and the ID of the object), combines it with the current user id, and then works out which combination of generic / primitive capabilities needs to be satisfied. In other words, map_meta_cap takes in one contextual capability and then converts it (or “maps” it) to one or more generic / primitive capabilities. current_user_can then returns true only if the current user has all those generic / primitive capabilities.

In the last example above then, map_meta_cap() has “mapped” ‘edit_posts’ to ‘edit_others_posts’ and ‘edit_private_posts’.

Okay, enough of the general background. Time to look at the capabilities for a custom post type.

How WordPress checks custom post type capabilities

When a post type is registered (using register_post_type) its arguments are stored in the global variable $wp_post_types, which is an array of post type objects indexed by the post type.

For each post type object, its capabilities can be found stored in its ‘cap’ property. Now, if we look at the ‘cap’ property of the post type objects ‘post’, ‘nav_menu_item’ and ‘revision’, it’s the same in each case – an object with the following properties:

[edit_post] => edit_post
[read_post] => read_post
[delete_post] => delete_post
[edit_posts] => edit_posts
[edit_others_posts] => edit_others_posts
[publish_posts] => publish_posts
[read_private_posts] => read_private_posts
[read] => read
[delete_posts] => delete_posts
[delete_private_posts] => delete_private_posts
[delete_published_posts] => delete_published_posts
[delete_others_posts] => delete_others_posts
[edit_private_posts] => edit_private_posts
[edit_published_posts] => edit_published_posts
[create_posts] => edit_posts

But if we look at the ‘cap’ property of the ‘attachment’ post type, there’s one small difference. Everything else is the same, but we find that [create_posts] => edit_posts is replaced by [create_posts] => upload_files.

Now, littered throughout WordPress core you’ll find checks like this:

if ( current_user_can( $post_type_object->cap->edit_posts ) ) {
    //do something
}

…and…

if ( current_user_can( $post_type_object->cap->create_posts ) ) {
    //do something
}

Taking the first example then, we will get the same result whether we are dealing with a post of type ‘post’ or a post of type ‘attachment’. current_user_can looks at the ‘edit_posts’ property of the cap object and sees that it must check the capability ‘edit_posts’.

The second example is a bit different. If we happen to be dealing with a standard post, then current_user_can looks at the ‘create_posts’ property of the cap object and sees that it should check whether the user has the capability ‘edit_posts’. But if we are dealing with an attachment, then current_user_can is told to check whether the user has the capability ‘upload_files’.

That’s simple enough, then. When we set capabilities for a custom post type we are creating the cap property of the post type object that current_user_can uses when working out which capability to check. Or to put it another way, WordPress core regularly invites you, the creator of the custom post type, to tell it what capability you would like checked. Basically it’s saying, “I think I would normally check the ‘edit_posts’ capability in this situation, but you can tell me to check for another capability if you like”.

Setting capabilities for customs post types

Finally! We can start looking at how we set capabilities. There are three arguments to register_post_type that are relevant: ‘capability_type’, ‘map_meta_cap’ and ‘capabilities’.

There’s usually a fundamental direction you need to take when setting up capabilities:

Is your custom post type going to use the standard capabilities already available in core, or are you going to create your own?

If you use your own capabilities then it’ll be down to you to make sure those capabilities are correctly added to the appropriate user. So for example if you have a plugin that introduces a custom post type with non-standard capabilities, you’ll have to add and remove those capabilities on plugin activation and deactivation. That adds a bit of work, but it has one important advantage – administrators then have the option of changing how capabilities are allocated to suit their site’s own needs. If you only use the default WordPress capabilities, administrators can’t really fiddle around with how capabilities are allocated to roles / users: that would mess with the way permissions are handled elsewhere on their site.

Standard or custom capabilities, then? No right answer as it depends on the circumstances – the choice is yours. Lets look at how we handle each approach.

Using the standard WordPress capabilities

The simplest case is when you want your custom post type to have the same capabilities as the standard post type. You don’t have to do anything. Leave ‘capability_type’, ‘map_meta_cap’ and ‘capabilities’ unset.

Let’s imagine though that you want something different. Perhaps you want your custom post type to be accessible, editable and deletable only by administrators . We could do that by setting everything in the ‘capabilities’ array to ‘manage_options’:

$args = array(
    // All the other args, and...
    'capabilities'  => array(
        'edit_post' => 'manage_options',
        'read_post' => 'manage_options',
        'delete_post' => 'manage_options',
        'edit_posts' => 'manage_options',
        'edit_others_posts' => 'manage_options',
        'publish_posts' => 'manage_options',
        'read_private_posts' => 'manage_options',
        'read' => 'manage_options',
        'delete_posts' => 'manage_options',
        'delete_private_posts' => 'manage_options',
        'delete_published_posts' => 'manage_options',
        'delete_others_posts' => 'manage_options',
        'edit_private_posts' => 'manage_options',
        'edit_published_posts' => 'manage_options',
        'create_posts' => 'manage_options'
    ),
    'map_meta_cap' => true,
);

register_post_type( $args );

Notice one thing though – we have also set ‘map_meta_cap’ to true. I actually think it’s a bad idea to leave it unset, because the default changes depending on what else you do. If you leave absolutely everything unset then ‘map_meta_cap’ defaults to true. If ‘capabilities’ isn’t empty though, ‘map_meta_cap’ defaults to false. So at least if you set it one way or the other you know where you stand. We want it set to true – but we’ll come to why later.

Setting custom capabilities

The simplest way to create custom capabilities is to use the argument ‘capability_type’. Let’s imagine we are registering a custom post type ‘book’. If we set ‘capability_type to ‘book’ and map_meta_cap to true, like this…

$args = array(
    // All the other args, and...
    'capability_type'  => 'book',
    'map_meta_cap' => true,
);

register_post_type( $args );

…then our capabilities are automatically set as per the capabilities for a post, but with ‘post’ replaced by ‘book’. In other words, the cap property of the ‘book’ post type object is set to:

[edit_post] => edit_book
[read_post] => read_book
[delete_post] => delete_book
[edit_posts] => edit_books
[edit_others_posts] => edit_others_books
[publish_posts] => publish_books
[read_private_posts] => read_private_books
[read] => read
[delete_posts] => delete_books
[delete_private_posts] => delete_private_books
[delete_published_posts] => delete_published_books
[delete_others_posts] => delete_others_books
[edit_private_posts] => edit_private_books
[edit_published_posts] => edit_published_books
[create_posts] => edit_books

The plural of book is books, which works fine. But if we have a post type like ‘story’ then ‘edit_posts’ is converted to ‘edit_storys’ because WordPress just adds an ‘s’ to make the plural. But WordPress has us covered. We have the option to set ‘capability_type’ to an array containing the singular and the plural forms – array( ‘story, ‘stories’ ). With that, ‘edit_posts’ is converted to ‘edit_stories’ and so on.

Don’t forget that if you set custom capabilities, these new capabilities must be added to the appropriate users – otherwise nobody will pass any of the current_user_can checks.

Using ‘capability_type’ and ‘capabilities’ together

In our two examples above, we have used either ‘capability_type’ or ‘capabilities’, leaving the other unset. There’s no problem with using them both though, and sometimes it’s helpful to do so. The only thing to remember is that any arguments set in the ‘capabilities’ array overwrite whatever the ‘capability_type’ argument creates. For example, we could set our args as:

$args = array(
// All the other args, and...    
    ‘capability_type’ => ‘book’,
    ‘capabilities’ => array(
        ‘edit_others_posts’ => ‘manage_options'
    ),
)

register_post_type( $args );

This would set the post type capabilities as:

[edit_post] => edit_book
[read_post] => read_book
[delete_post] => delete_book
[edit_posts] => edit_books
[edit_others_posts] => manage_options    // <= NB over-written...
[publish_posts] => publish_books
[read_private_posts] => read_private_books
[read] => read
[delete_posts] => delete_books
[delete_private_posts] => delete_private_books
[delete_published_posts] => delete_published_books
[delete_others_posts] => delete_others_books
[edit_private_posts] => edit_private_books
[edit_published_posts] => edit_published_books
[create_posts] => edit_books

I’m not saying I can think of a case when you might specifically want to do this – it’s just an example to show how anything in ‘capabilities’ takes precedence over what ‘capability_type’ comes up with.

A more useful example might be something like this:

$args = array(
// All the other args, and...    
    ‘capability_type’ => ‘book’,
    ‘capabilities’ => array(
        ‘edit_others_posts’ => ‘manage_books',
        'publish_posts' => 'manage_books',
        'read_private_posts' => 'read',
        'read' => 'read',
        'delete_posts' => ‘manage_books',
        'delete_private_posts' => ‘manage_books',
        'delete_published_posts' => ‘manage_books',
        'delete_others_posts' => ‘manage_books',
        'edit_private_posts' => ‘edit_books',
        'edit_published_posts' => ‘edit_books',
    ),
)

register_post_type( $args );

Note we haven’t set all the capabilities in the array – just those we need to over-write. We end up with the post type capabilities set as:

[edit_post] => edit_book
[read_post] => read_book
[delete_post] => delete_book
[edit_posts] => edit_books
[edit_others_posts] => manage_books         // <= over-written...
[publish_posts] => manage_books             // <= over-written...
[read_private_posts] => read                // <= over-written...
[read] => read                              // <= over-written...
[delete_posts] => manage_books              // <= over-written...
[delete_private_posts] => manage_books      // <= over-written...
[delete_published_posts] => manage_books    // <= over-written...
[delete_others_posts] => manage_books       // <= over-written...
[edit_private_posts] => edit_books          // <= over-written...
[edit_published_posts] => edit_books        // <= over-written...
[create_posts] => edit_books

By doing this we are grouping capabilities into just three custom capabilities that we then allocate to users (props to Justin Tadlock for this approach).

The final consideration – map_meta_cap

I suspect that in the vast majority of cases ‘map_meta_cap’ should be set to true. That means that when core checks a contextual / meta capability (e.g., current_user_can( ‘edit_post’, $post_id ) ), WordPress will convert that contextual / meta capability to generic / primitive capabilities as if it was looking at a standard post – and it will then convert those to whatever you have specified in your custom post. Hard to follow? Let’s take an example.

Say we’ve set ‘map_meta_cap’ to true for our custom post type with ‘capability_type’ of ‘book’, and we have left the ‘capabilities’ array unset. We know that sets the post type capabilities like this:

[edit_post] => edit_book
[read_post] => read_book
[delete_post] => delete_book
[edit_posts] => edit_books
[edit_others_posts] => edit_others_books
[publish_posts] => publish_books
[read_private_posts] => read_private_books
[read] => read
[delete_posts] => delete_books
[delete_private_posts] => delete_private_books
[delete_published_posts] => delete_published_books
[delete_others_posts] => delete_others_books
[edit_private_posts] => edit_private_books
[edit_published_posts] => edit_published_books
[create_posts] => edit_books

Now imagine we find ourselves in a similar situation to one I described earlier: core happens to run the check current_user_can( ‘edit_post’, 892 ) where post 982 is one of our ‘book’ posts and it happens to be published. Because we have ‘map_meta_cap’ set to true, the ‘edit_post’ check will return true if:

  1. The user has the capability ‘edit_published_books’; AND
  2. If the user is not the author of the post, the user has the capability of ‘edit_others_books’.

Because we set the capability_type to ‘book’ the checks are on ‘edit_published_books‘ not ‘edit_published_posts’, and ‘edit_others_books‘ instead of ‘edit_others_posts’. But with ‘map_meta_cap’ set to true, WordPress is still applying the same logic to convert contextual to generic capabilities – just as if we were looking at a standard post.

So what happens if we set ‘map_meta_cap’ to false instead? Whenever a contextual capability is checked, it is not converted by map_meta_cap(), but left as is. That means that in effect WordPress would just check ‘edit_book’ in our example. This is generally no good to anyone. You could in theory allocate ‘edit_book’, ‘read_book’ and ‘delete_book’ capabilities to users, but that’s pointless. All you’re doing is removing the contextual check – you might as well use ‘edit_books’ instead. For that reason, you should never allocate any contextual capabilities to users, only generic / primitive.

Which leaves us with the question: Why would you ever set map_meta_cap to false?

Answer: When you want to define your own mapping / converting to follow rules that are different from the built-in core rules.

The output of the map_meta_cap function can be filtered using the ‘map_meta_cap’ filter. So let’s say we want to set our own contextual check rules for our custom post type of ‘book’. We set ‘map_meta_cap’ to false, and we also set a filter to define the capabilities we want checked. Something like this:

function my_custom_meta_capabilities_mapping( $caps, $cap, $user_id, $args ) {
    if ( in_array( $cap, array( 'read_book', 'edit_book', 'delete_book' ) ) ) {
        $post = get_post( $args[0] );         // the object id is stored in $args[0]
        if ( ! $post ) {
            // if this isn't a post then we should make current_user_can returns false
            $caps[] = 'do_not_allow';
        } elseif( 'read_book' == $cap ) {
            if ( // Some conditions met ) {
                $caps[] = // Capability A
                $caps[] = // Capability B
            } else {
                $caps[] = // Capability C
            }
        } elseif( 'edit_book' == $cap ) {
            if ( // Some conditions met ) {
                $caps[] = // Capability D
                $caps[] = // Capability E
            } else {
                $caps[] = // Capability F
            }
        } elseif( 'delete_book' == $cap ) {
            if ( // Some conditions met ) {
                $caps[] = // Capability G
            } else {
                $caps[] = // Capability H
            }
        }
    }
}
add_filter( 'map_meta_cap', 'my_custom_meta_capabilities_mapping', 10, 4 );

Wrapping up

That about covers it. Let’s see if I can pick the most important things to hang on to in a few pithy bullets:

  • Primitive capabilities are generic – they apply to a post type in general. Meta capabilities are both specific and contextual – they apply to a single object, and are converted to one or more primitive capabilities depending on the context.
  • When setting post type capabilities, the choice is generally between using the built-in capabilities or creating your own. Adding your own capabilities means someone has to allocate them to roles / users – either the user, or you the developer. The advantage is flexibility – others can choose to change how capabilities are allocated without messing with the default permissions set-up in WordPress.
  • The easy way to create custom capabilities is with ‘capability_type’. All capabilities are then generated automatically. You don’t have to set the ‘capabilities’ array at all if you are happy with the standard structure of capabilities.
  • Any capabilities you set using the ‘capabilities’ array will over-write the equivalent capabilities generated by ‘capability_type’.
  • Beware of leaving ‘map_meta_cap’ unset. The default changes depending on what else you have set. If you’ve set it one way or the other at least you know where you stand.
  • Never allocate contextual / meta capabilities to a role or user.
  • Unless you want to define your own “mapping” rules for contextual / meta capabilities (and you’ll need to use the ‘map_meta_cap’ filter to do so), map_meta_cap should always be set to true.

Leave a Reply

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