Mastering WordPress Roles and Capabilities

Johan Satgé
Share

User management on WordPress is based on roles and capabilities.

A role is an entity made of a unique name and a set of capabilities. Each capability is used to define if the role has access to a particular feature of the platform.

Let’s take a closer look at how WordPress Roles and Capabilities work.

Under the Hood

Storing the Roles

The list of default roles and capabilities is available on the WordPress Codex.

The database stores this list in the wp_options table.

It uses the serialized wp_user_roles key.

wp_options table

The unserialized data looks like this:

array(
    'administrator' => array(
        'name'         => 'Administrator',
        'capabilities' => array(
            'switch_themes'          => true,
            'edit_themes'            => true,
            'activate_plugins'       => true,
            'edit_plugins'           => true,
            'edit_users'             => true,
            // [...]
        )
    ),
    'contributor' => array(
        'name'         => 'Contributor',
        'capabilities' => array(
            'delete_pages'           => true,
            'delete_others_pages'    => true,
            'delete_published_pages' => true,
            'delete_posts'           => true,
            // [...]
        )
    ),
    // [...]
);

This meta is automatically set when installing a new WordPress site.

When WordPress starts, the WP_Roles class loads the list from the database.

This occurs between the plugins_loaded and the init hooks.

Linking Users to Roles

WordPress uses a meta_key, stored in the wp_usermeta table, to link a user to his role.

wp_usermeta table

Once unserialized, the meta looks like this:

array(
    'administrator' => true
)

Note that WordPress uses an array, although a user can only have one role at a time, we will see later why.

Also, keep in mind that the wp_ part of the meta key is the prefix of the current blog.

(We can get it by using the $GLOBALS['wpdb']->get_blog_prefix() function).

On a multisite installation, this would allow a user to use different roles on different instances:

  • wp_capabilities => a:1:{s:13:"administrator";b:1;}
  • wp_10_capabilities => a:1:{s:11:"contributor";b:1;}
  • wp_15_capabilities => a:1:{s:10:"subscriber";b:1;}
  • [...]

This rule also applies to the wp_user_roles entry that we have seen before, in the wp_options table.

Finally, we can see the wp_user_level meta along with the role.

It was used to handle roles in old WordPress versions, and is now deprecated.

Working with Capabilities in the Core

We have seen how roles are loaded and linked to users; from there, WordPress is able to get the capabilities of a given user, when needed.

Several default capabilities are hard-coded in the WordPress core.

For instance, when loading the plugin screen, it will check if the current user can manage plugins, by running the following code:

if (!current_user_can('activate_plugins'))
{
    wp_die(__('You do not have sufficient permissions to manage plugins for this site.'));
}

Roles are never hard-coded; a role is only a capabilities wrapper, it only exists in the database.

Working with Roles & Capabilities: the WordPress API

Access the API

WordPress provides the following global functions to help us work with roles.

current_user_can()

Checks if the current user owns the required capability.

add_action('init', function()
{
    if (current_user_can('install_plugins'))
    {
        echo 'you can install plugins';
    }
    else
    {
        echo 'You cannot install plugins';
    }
});

WP_User::has_cap

Checks if a specific user owns a capability.

add_action('init', function()   
{
    $user = get_user_by('slug', 'admin');
    if ($user->has_cap('install_plugins'))
    {
        echo 'Admin can install plugins';
    }
    else
    {
        echo 'Admin cannot install plugins';
    }
});

We can note that current_user_can uses this function.

get_editable_roles()

Returns available roles.

add_action('admin_init', function()
{
    $roles = get_editable_roles();
    var_dump($roles);
});

The list may be overriden with the editable_roles filter, so we should not rely on this function to get the complete roles list on a website.

Note the usage of the admin_init hook, as the function is not loaded yet on the init one.

get_role()

Gets a WP_Role object from its slug.

add_action('init', function()
{
    $role = get_role('administrator');
    var_dump($role);
});

// This will print:
// WP_Role Object
// (
//     [name] => administrator
//     [capabilities] => Array
//         (
//             [switch_themes] => 1
//             [edit_themes] => 1
//             [activate_plugins] => 1
//             [edit_plugins] => 1
//             [...]

WP_Role::has_cap()

Checks if a role has the required capability.

add_action('init', function()
{
    $role = get_role('administrator');
    var_dump($role->has_cap('install_plugins')); // Prints TRUE
});

Customizing the API

WordPress also offers a complete API to customize the roles and their capabilities.

add_role()

Registers a new role in the database.

add_action('init', function()
{
    add_role('plugins_manager', 'Plugins Manager', array(
        'install_plugins',
        'activate_plugins',
        'edit_plugins'
    ));
});

remove_role()

Removes the required role from the database, if it exists.

add_action('init', function()
{
    remove_role('plugins_manager');
});

WP_Role::add_cap()

Adds a capability to a role.

add_action('init', function()
{
    $role = get_role('contributor');
    $role->add_cap('install_plugins');
});

This may be a core capability (install_plugins, edit_posts, …) or any custom string (my_awesome_plugin_cap).

It allows us to register as many custom capabilities as we need for our plugins.

WP_Role::remove_cap()

Removes a capability from a role, if it exists.

add_action('init', function()
{
    $role = get_role('contributor');
    $role->remove_cap('install_plugins');
});

WP_User::add_role()

Adds a role to the given user.

add_action('init', function()
{
    $user = get_user_by('slug', 'admin');
    $user->add_role('contributor');
});

This function allows you to theoretically set many roles on the same user.

As the WordPress backend only displays and manages one role per user, we should not add several roles for a user, and always use WP_User::remove_role() before adding a new one.

WP_User::remove_role()

Removes a role from the given user.

add_action('init', function()
{
    $user = get_user_by('slug', 'admin');
    $user->remove_role('administrator');
});

WP_User::add_cap()

Adds a capability to the given user.

add_action('init', function()
{
    $user = get_user_by('slug', 'admin');
    $user->add_cap('my_custom_cap');
});

wp_usermeta table with custom caps

This can be useful if we want to add a single capability to a user, without having to create a complete role.

WP_User::remove_cap()

Removes a capability from the given user.

add_action('init', function()
{
    $user = get_user_by('slug', 'admin');
    $user->remove_cap('my_custom_cap');
});

A Few Issues with the WordPress API

Everything looks fine with the functions we have seen, except one thing: database access and performance.

The main concern we have when working with roles and capabilities is when should we trigger our code?

To explain this, let’s have a look at the code of the WordPress core.

First, we want to add a new, empty role:

add_action('init', function()
{
    add_role('plugins_manager', 'Plugins Manager', array());
});

Here are the first lines of the add_role function (which actually redirects to WP_Roles::add_role):

public function add_role( $role, $display_name, $capabilities = array() ) {
        if ( isset( $this->roles[$role] ) )
            return;

If we add a new role, the add_role function runs once, and then does nothing.

Next, let’s say we want to add a capability to our freshly created role:

add_action('init', function()
{
    $role = get_role('plugins_manager');
    $role->add_cap('install_plugins');
});

The WP_Role::add_cap() function in WordPress 4.2.2 looks like this:

public function add_cap( $role, $cap, $grant = true ) {
    if ( ! isset( $this->roles[$role] ) )
        return;

    $this->roles[$role]['capabilities'][$cap] = $grant;
    if ( $this->use_db )
        update_option( $this->role_key, $this->roles );
}

It updates the $this->roles object, but we can also see that the database will be updated each time our code runs, even if our new capability has already been registered!

This means that if we care about performance, all the code we produce to customize roles and capabilities should not run on each page load.

Workarounds

There are several options to avoid these database problems.

Working with Plugin Activation

WordPress allows plugin authors to trigger code when a plugin is enabled from the backend, by using the register_activation_hook() function.

Let’s create a sample plugin:

/*
Plugin Name: Our sample role plugin
*/
register_activation_hook(__FILE__, function()
{
    $role = add_role('plugins_manager', 'Plugins Manager', array());
    $role->add_cap('install_plugins');
});

This code would run only once, when enabling the plugin on the website.

Now, we have to keep in mind that this solution depends on plugin activation and deactivation.

What would occur if the plugin is already in production, or if its reactivation is omitted when pushing the update?

In fact, this solution depends on the database too, and requires an extra step when pushing code.

Bypassing the WordPress Database

There is a second, undocumented solution, that can work well in some cases.

Let’s have a last look at the WordPress core, when the WP_Roles object loads the roles from the database on WordPress boot:

protected function _init() {
    global $wpdb, $wp_user_roles;
    $this->role_key = $wpdb->get_blog_prefix() . 'user_roles';
    if ( ! empty( $wp_user_roles ) ) {
        $this->roles = $wp_user_roles;
        $this->use_db = false;
    } else {
        $this->roles = get_option( $this->role_key );
    }

Before getting the data from the database, WordPress checks for the $wp_user_roles global variable.

If set, WordPress will use its content, and block database usage by setting the $this->use_db variable to false.

Let’s try this, by keeping only a new, restricted administrator role:

/*
Plugin Name: Our sample role plugin
*/
$GLOBALS['wp_user_roles'] = array(
    'administrator' => array(
        'name' => 'Administrator',
        'capabilities' => array(
            'activate_plugins' => true,
            'read' => true,
        )
    )
);

When loading the backend, we can see it has kept the definition of our custom role:

custom backend

This solution solves the database problem, but may introduce some others:

  • Plugins which use the native API may not behave correctly.
  • We have to manually set the definition of each role, even the ones we don’t want to change.

However, when building a custom WordPress application that needs a custom, static list of roles, this could be a possible solution:

  • The roles definition can be versioned with the code.
  • Pushing new code on an environment will automatically update the definition.
  • No more questions about the database.

Conclusion

In this article, I’ve presented an overview of roles and capabilities usage in WordPress.

Although its complete API allows us to do almost whatever we want, the relation to the database remains the main concern.

We’ll have to keep this in mind when developing our plugins and themes.

What do you think about the way WordPress manages roles? I look forward to your feedback!