Custom post types and capabilities


In the previous tutorial, Developing with user roles and capabilities, you learned how to create and apply user roles, how to add/remove capabilities from a user role, and how to check user capabilities.

In this tutorial, you will learn about the built-in support for capabilities when you register a custom post type, and how you can refine these capabilities to suit your specific requirements.

Learning outcomes

  1. Explain the WordPress user roles and capabilities system
  2. Assign capabilities to an existing role
  3. Create a new User Role and assign capabilities to it

Comprehension questions

  1. When is the correct time to register a new role?
  2. How do you assign a capability to an existing role?
  3. How do you register a new role and assign capabilities to it?
View video transcript

Hey there, and welcome to Learn WordPress.

In this tutorial, you’ll be learning how to restrict access to custom post types via the built in capability system. You will learn how the different capabilities are assigned to a Custom Post Type, and how to manage them, as well as how to add Custom Post Type capabilities to new or existing roles.

Let’s say you need to create a plugin that allows a site owner to add the following functionality to a WordPress site. The ability to define a Custom Post Type called Story. The ability to create users with a writer role who could only create, edit and delete unpublished stories. The writer role should also be able to publish their own stories, but not edit or delete stories. Once published. Any site administrators should be able to create, edit and delete any stories, regardless of whether they are published or not, or who they were created by.

To create the story Custom Post Type, you might have some code in your plugin that looks like this. Here you’re using the register_post_type function to register the story Custom Post Type, and passing in some specific arguments to turn certain things on and off.

When a Custom Post Type is registered in this way, it inherits the capabilities of the Post post type. To see these capabilities in your dashboard during development, you might register a sub menu page that displays the capabilities of the story Custom Post Type, which is only available to admins. Inside the callback function for this page, you could access the Custom Post Type object from the wp_post_types global array variable. You could then print out the capability_type, map_meta_cap and cap properties of the story post type. With the plugin active when logged in as an administrator user, this is what you might see when you visit the page in your dashboard. As you can see, the capability type inherits from post map_meta_cap is set to true and the cap property contains an array of capabilities that are inherited from the default WordPress capabilities for a post.

To understand how these capabilities are mapped to the Custom Post Type, you need to understand how these three properties are set up. To do this, take a look at the register_post_type function, which currently exists at line 1679 of the wp-includes/post.php file. On line 1694 the WP_Post_Type object is created and the args array is passed to the class constructor.

If you open the file for the WP_Post_Type class, you’ll see the following code at around line 541. Here if the capabilities argument is empty, and the map_meta_cap argument is null and the capability_type argument is set to either post or page, then the map_meta_cap argument is set to true. If you scroll back up to line 480, you can see that the default value for capability_type is post. So because your Custom Post Type doesn’t pass any of these arguments, then map_meta_cap is set to true. Later on the cap property is set to the result of the get_post_type_capabilities function. This function accepts the args array as an argument and returns an array of capabilities. So because the capability_type argument is set to post and the map_meta_cap argument is set to true, then the cap property on the story Custom Post Type will be set to an array of capabilities that are mapped to the default WordPress capabilities for a post.

You can control these capabilities by passing relevant values to the three arguments that are used above as described in the developer handbook entry for register_post_type. Possible arguments are capability_type, capabilities, and map_meta_cap. The first step in setting custom capabilities for your Custom Post Type is to set the capability_type argument to something other than post. In this case, you could set it to story. If you now take a look at the cap property on your Custom Post Type, you will see that the capabilities have changed. However, it’s not using the correct pluralization of story. So you need to explicitly set the plural version of the capability type by passing in an array of values, the first being the singular name and the second being the pluralization.

So we will change this to an array, story and stories.

Now if you take a look at the cap property on your Custom Post Type, you will see that the capabilities have changed again. Notice that this list does not include all the same capabilities when they were inherited from post. This is because by setting a capability type the first conditional check in the WP_Post_Type class you looked at earlier returns false. And so the map_meta_cap argument is now set to false, which means the additional capabilities are not mapped for the Custom Post Type.

Now it’d be a good time to talk about the different types of capabilities. There are three types of capabilities available to custom post types. Meta capabilities are capabilities that are mapped to primitive capabilities. The three meta capabilities are edit_post, read_post, and delete_post. As an example, the edit_post meta capability is mapped to primitive capabilities like edit_posts and edit_others_posts. Because the meta capabilities are automatically mapped to certain primitive capabilities, it’s generally recommended not to grant the meta capabilities directly to users or roles, and rather to grant any of the primitive capabilities.

There are two types of primitive capabilities; those that are automatically mapped to a meta capability when you register a Custom Post Type with a specific capability_type, and those that are not. To understand the differences take a look at the get_post_type_capabilities function in the wp-includes/post.php file. This is the same function you saw earlier in the WP_Post_Type class that builds up the capabilities object for the cat property on the Custom Post Type object. Here you can see that the default capabilities are always set, which includes the three meta capabilities and five primitive capabilities, then an additional six primitive capabilities are added. These are known as the primitive capabilities used within the map_meta_cap function. Below that the create_post capability is automatically mapped to edit posts for a total of 15 possible capabilities.

You might be wondering what the map_meta_cap function is and what it does. The map_meta_cap function is used whenever the current_user_can function is called to check if a user role has a specific capability. If you dive into the code underneath the current_user_can function, you’ll see it eventually calls the has_cap function of the WP_User class, which in turn calls the map_meta_cap function. map_meta_cap maps a capability to the primitive capabilities required of a given user to satisfy the capability being checked, based on the context of the check. The map_meta_cap function does not check whether the user has the required capabilities, it just returns what the required capabilities are.

To help explain this, let’s look at an example. Let’s say you want to use current_user_can to check if the current user can edit a specific post by checking against the edit_post meta capability. Based on the context of the post, the result for this check can depend on a few factors; is the user the author of the post, and is the post already published. When checking against the edit_post capability, the map_meta_cap function will check all these factors and return the correct set of primitive capabilities that the user must have to allow editing of the post. So if the post is written by someone else, and it’s published, it returns an array containing the edit_others_posts and edit_published_posts capabilities. In this case, the user would not only need the edit_post capability, but also the edit_others_posts and edit_published_posts capabilities in order to edit this post.

If you look back at the different lists of capabilities that you might need for your writer, you’ll notice that some exist as meta capabilities, some is the automatically mapped primitive capabilities, and some exist as the primitive capabilities mapped inside map_meta_cap. As discussed earlier, you should not grant the meta capabilities directly to users or roles, and rather to add any of the primitive capabilities. Therefore, you’re going to want to add the following automatically mapped primitive capabilities; edit_stories, delete_stories, publish_stories, and read_private_stories. But you’re also going to want to add the following primitive capabilities mapped inside map_meta_cap; the read capability, the delete_private_stories capability and the edit_private_stories capability.

There are two ways you can do this. You can add additional primitive capabilities you need by using the capabilities argument of the register_post_type function. So under your capability type, you could add the specific capabilities you need. If you now take a look at the cap property of the Custom Post Type object, you’ll see that the capabilities object now includes all the capabilities you added. You can now add these capabilities to your writer role.

The other option is to set the map_meta_cap argument to true. This will automatically also map the meta capabilities to the primitive capabilities used inside map_meta_cap. With this option, all the possible capabilities will be mapped. And you can then add the specific ones you need to your user roles.

So you can take out the capabilities include map_meta_cap, and if we inspect the list of capabilities, they are all there.

Ultimately, it’s up to you which option you choose. The capabilities argument is a bit more flexible, and provides more fine grained control. But using the map_meta_cap argument requires less work on your part. It’s also worth noting that either option will not automatically add the capabilities to any role, you still need to define that yourself.

One other thing change when you set the custom capability type argument, your admin user can no longer access stories in the dashboard. This is because the administrator user role has not been given the capabilities to access stories, because your admin user role should be able to access everything. Now it’d be a good time to add all the capabilities to the admin role. This is another reason to consider using the map_meta_cap argument as it will automatically map all the capabilities, which you can assign to the admin role.

As you learned in the developing WordPress user roles and capabilities tutorial, the right place to do this is inside a plugin activation hook. First, deactivate your plugin so that you can activate it to trigger the activation hook.

Inside plugins we can deactivate.

Then create the register_activation_hook in your plugin and use it to assign the required capabilities to the administrator user. Because there are so many capabilities to add it’s a good idea to store them in an array, and then loop through them to add them to the role. If you ever need to add or remove capabilities, you can just update the array. You do not need to add the read capability as this is already added to the administrator role. At the same time, you can add the deactivation hook to remove the capabilities from the administrator role, if the plugin is ever deactivated. Now you can create the writer role and only apply the capabilities that the writer needs.

Inside the activation hook callback function, you can create the writer role using the add_role function and add the relevant capabilities.

As a reminder, the writer can only create, edit and delete and publish stories. And writers should also be able to publish their own stories, but not edit or delete stories once published. Note that you can do this inside an existing activation hook callback function, or you can create a new one. At the same time, it’s a good idea to update the deactivation callback function or create a new one to remove the role when the plugin is deactivated. Now activate the plugin and check that the admin can create, edit, publish and edit a published story.

So if we activate, admins can now access stories and create new stories.

Then create a new user and assign the writer role to the user.

Log in as the writer and check that the writer can create, edit and publish the story, but cannot delete a published story.

They also should not be able to edit or delete anyone else’s stories.

So let’s create the writer user give them a simple password and assign the writer role. And then login as the writer

writer can access stories but cannot edit the admin story. They can create a new story.

They can save and edit their stories. But once published, writers cannot edit their own stories.

Your specific requirements will determine how you set your capabilities for your custom post types. Perhaps you don’t need the administrator role to access stories, then you could just have manually set the additional capabilities you need and not use the magnetic app argument. Either way, whatever capabilities you configure will only be applied once you add them to a role.

Generally your process for setting up Custom Post Type capabilities will be to set the correct capability type or types. Then use either the capabilities argument or the map meta cap argument to set up any additional capabilities require and then to either update an existing role or create a new custom role and assign the relevant capabilities needed.

Always remember to set up roles and or capabilities inside an activation hook callback function and remove them inside a deactivation hook callback function.

You can read up on how capabilities can be mapped on custom post types in the parameter detail information section of the register_post_type documentation in the WordPress developer handbook, as well as the function reference entry on the get_post_type_capabilities function.

Happy coding!

Introduction

Hey there, and welcome to Learn WordPress!

In this tutorial, you’ll be learning how to leverage the WordPress user roles and capabilities system.

After a brief introduction to the default WordPress roles and capabilities, how they are created and managed, and how to assign them to users, you will learn how to add capabilities to an existing role, and how to create a new user role and add capabilities to it.

WordPress User Roles and Capabilities

The WordPress user roles and capabilities system is a powerful tool for managing access to your site. It allows for user roles with specific capabilities, and you can then assign those roles to users. This allows you to create a hierarchy of users, with some users having more access than others.

WordPress comes with several default user roles, namely Administrator, Editor, Author, Contributor, and Subscriber.

There is also one additional role, but this is only available in multisite installations, and that’s the Super Admin role.

Each of these roles has a set of capabilities, which are the things that the user can do in a WordPress site. For example, the Administrator role has the capability to manage users, while the Subscriber role does not.

When a new user is created in the WordPress dashboard, they are assigned the Subscriber role by default. You can change this by assigning a different role. Once assigned to a role, the user will have the capabilities that are associated with that role.

You can read up on the full list of default user roles and capabilities in the official WordPress Documentation on Roles and Capabilities.

Roles and Capabilities under the hood

The WordPress user roles and capabilities system is stored in the database during the WordPress installation process. The roles and capabilities are stored as a serialized array as site options the options table, in the user_roles option. The prefix for this option will depend on the table prefix configured in wp-config.php, or if this is a multisite installation. By default, on a single site installation, the prefix will be wp_, so the option name will be wp_user_roles.

If you extract the value of the user_roles option, and unserialize the array, you will see that the array keys are the role names, and the values are arrays of capabilities.

Whenever an authenticated user is attempting to do something, this action is checked against the user’s role capabilities, using the current_user_can function. If the user has the capability to perform the action, they will be allowed to do so. If they do not have the capability, they will be denied access.

Take a look at line 270 of the wp-admin/includes/post.php file. Here you will see that the edit_post capability is checked before a post is edited/update. If the user does not have the edit_post capability for this post, they will be informed that they do not have the required access to edit it.

Assign capabilities to an existing role

Let’s say that you want to allow your editors to be able to do something that only admins can do, maybe install and update plugins. By default, the Editor role does not have the activate_plugins or update_plugins capability.

You can add this capability to the Editor role by using the add_cap method of the WP_Role class.

However, doing so will add this capability to the user_roles stored in the database, so it’s recommend to run this on something like plugin activation using the register_activation_hook function.

register_activation_hook( __FILE__, 'wp_learn_add_custom_caps' );
function wp_learn_add_custom_caps() {
    // do something
}

The register_activation_hook requires the path to the file that contains the hook callback, and the hook callback function name.

Then you can add the capabilities to the Editor role by using the get_role function to get the role object, and then using the add_cap method to add the capabilities.

register_activation_hook( __FILE__, 'wp_learn_add_custom_caps' );
function wp_learn_add_custom_caps() {
    // gets the author role
    $role = get_role( 'editor' );
    $role->add_cap( 'activate_plugins' );
    $role->add_cap( 'update_plugins' );
}
  1. First, you should hook into the register_activation_hook action, so that the code is only executed when the WP dashboard is loaded.
  2. Then, you get the role object by using the get_role function. The get_role function takes the role name as a parameter, and returns the role object.
  3. Finally, you use the role’s add_cap method to add the activate_plugins and update_plugins capabilities to the role object.

Adding this code to a plugin, and activating the plugin, triggering the activation hook, will update the Editor role with the new plugin capabilities.

If you switch to an editor user, you will see that the editor user can now update plugins.

[Show how editor can active plugins]

Note that because adding capabilities to a role is a permanent change, you should only do this when the plugin is activated, and not on every page load. Also, if you want to remove a custom role, it’s a good idea to do so on plugin deactivation, using the register_deactivation_hook function.

register_deactivation_hook( __FILE__, 'wp_learn_remove_custom_caps' );
function wp_learn_remove_custom_caps() {
    $role = get_role( 'editor' );
    $role->remove_cap( 'activate_plugins' );
    $role->remove_cap( 'update_plugins' );
}

This is useful for two reasons. Firstly, you can add and remove the capabilities when the plugin is activated and deactivated, which is useful when testing whether the capabilities you’ve set are what you need. Secondly, if the user deactivates your plugin, the capabilities will be removed, cleaning up the changes your plugin has made.

Create a new User Role and assign capabilities to it

Just as it is possible to assign existing capabilities to roles, you can also create your own custom roles, and assign capabilities to them. This is useful if you want to create a role that has a specific set of capabilities, and you don’t want to use an existing role.

For example, let’s say you want a user role who can only activate and update plugins, say an assistant to the administrator. You can create a new role, using the add_role function, and assign the activate_plugins and update_plugins capabilities to it.

register_activation_hook( __FILE__, 'wp_learn_add_custom_role' );
function wp_learn_add_custom_role() {
    add_role(
        'assistant',
        'Assistant',
        array(
            'read'         => true,
            'activate_plugins'   => true,
            'update_plugins' => true,
        ),
    );
}

Again, because the new role will be stored in the database, you should only run this code when the plugin is activated, and not on every page load.

Notice how the array of capabilities includes the read capability. This is because the read capability is required for a user to be able to access the dashboard. Regardless of any other capabilities a user has, if they do not have the read capability, they will not be able to access the dashboard menu, in order to perform a specific task they should be able to.

You can also include a deactivation hook to remove the role when the plugin is deactivated, using the remove_role function.

register_deactivation_hook( __FILE__, 'wp_learn_remove_custom_role' );
function wp_learn_remove_custom_role() {
    remove_role( 'assistant' );
}

Create a new user, with the Assistant capability. Then switch users, and notice how all that user can do is manage plugins

Summary

For more information on developing user roles and capabilities, see the Roles and Capabilities section of the Plugin developer handbook on developer.wordpress.org.

Happy coding.

Workshop Details


Presenters

Jonathan Bossenger
@psykro

WordPress Developer Educator at Automattic, full-time sponsored member of the training team creating educational content for developers on Learn WordPress. Husband and father of two energetic boys.