Mastering WordPress Roles and Capabilities
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.
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.
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');
});
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:
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!