This is a four-part series tutorial covering the WordPress users, roles and capabilities topic. The series will cover the architecture and design of user roles in WordPress; highlight the most important functions for interacting with users and managing roles and capabilities; and in the last tutorial, we are going to build a real-life example that demonstrates the usefulness of this API.
Introduction
This tutorial will be focused on building a practical solution using the WordPress roles and capabilities system. If you have missed the last two tutorials, I'd highly suggest that you check them out. The first part "WordPress Roles and Capabilities: The Basics" explains the design of this system; while the second part "WordPress Roles and Capabilities: Functions of Note" focuses on the functions and classes WordPress offers to interact with the system.
The solution proposed in this tutorial is the same I'm using for one of my premium WordPress plugins. I have chosen it after trying different approaches. It's simple, short and encapsulated in one class. You can easily adapt it for your own plugin. The code is available on GitHub; and is not licensed. It also comes with no warranties, and you are free to use and license it as you wish.
Step 1 The Scenario
We are building a WordPress plugin which has a special client admin panel. This admin panel should be accessible only to a limited set of users. These users can be selected by the blog administrator. He can enable different roles, users or all of them to access the client admin panel or features.
Apart from that, we need to have a restricted Media Library access for users with an access to the Client Panel. WordPress has a special capability to access and upload files to the Media Library: "upload_files
". However, this gives the user (or role) full access to the Media Library. This is not a good thing, especially that photos (or files) can't be hierarchized under different categories where you can restrict the access for each one.
We need to restrict the Media Library access only to the files the user has uploaded. He shouldn't have access to other users' uploads. This restriction should be applied only to the users who don't have the "upload_files
" capability. Other users and roles aren't concerned about this restriction since they already have a full access to the Media Library.
Our blog will have these four categories of users:
The first two sets of users are the ones who will not have access to the plugin Client Panel. I have highlighted the fact that there are users who have access to the Media Library, and a set which doesn't. It's essential that our solution doesn't affect the first two categories and leave their capabilities intact.
With that in mind, our class should do two things:
- Set the right permissions to the third and fourth set of users
- Enable a restricted Media Library access to the fourth category. It should make sure that after disabling the plugin or changing the users' rights, that this category of users gets back their original permissions.
Step 2 The Plugin Interface
Before creating our class, let's have a deeper idea about the plugin. The plugin has a pretty simple structure. It's composed of two different menus: One for the administrator and it serves as an administrative panel, accessible only for users with a capability of "manage_options
". The second menu is for clients and gives access to the Client Panel. This panel requires a "wptuts_client_page
" capability.
This capability doesn't exist in WordPress; we need to add and assign it to the specified users or roles. But before that, let's take a look into the plugin.
/* Plugin Name: WordPress Roles Plugin Plugin URI: https://github.com/omarabid/WordPress-Roles-Plugin Description: A WordPress Roles Plugin Author: Abid Omar Author URI: http://omarabid.com Version: 1.0 */ // Add an Admin user menu to the WordPress Dashboard add_action('admin_menu', 'wptuts_admin_menu'); function wptuts_admin_menu() { add_menu_page('Admin Access', 'Admin Access', 'manage_options', 'wptuts-admin', 'wptuts_admin_page'); } function wptuts_admin_page() { echo 'Admin Page'; } // Add a client user menu to the WordPress Dashboard add_action('admin_menu', 'wptuts_client_menu'); function wptuts_client_menu() { add_menu_page('Client Access', 'Client Access', 'wptuts_client', 'wptuts-client', 'wptuts_client_page'); } function wptuts_client_page() { echo 'Client Page'; }
We have two "admin_menu
" action hooks which add the Admin menu for both the administrator and client. We can shorten it to only one hook which adds both of them; but I prefer to separate the two. Each "add_menu_page
" function hooks to another function which will display the page content.
Next, we need to initialize our Roles class. The class code is placed in another file; and the initialization process is done during the "init
" hook which ensures that WordPress has finished loading.
The constructor function accepts three parameters:
-
$all
Boolean (Optional) If you want to give the client access to all users on the blog, you can set this to true and ignore the remaining parameters. -
$roles
Array (Optional) An array with roles' ids which will have access to the client panel. -
$users
Array (Optional) An array with users' usernames which will have access to the client panel.
// Loads and initialize the roles class add_action('init', 'wptuts_start_plugin'); function wptuts_start_plugin() { require_once('roles_class.php'); $all = false; $roles = array('subscriber'); $users = array(3); new wpttuts_roles($all, $roles, $users); }
Step 3 The Roles Class
The Properties
The class has only 3 properties: $all
, $roles
and $users
. These are the same variables that you pass to the constructor function. The value of the variables is not changed in our example; but in a real practical case, you may want to merge with another source. For this, you have the set_entities
function. You can accommodate it to your own needs.
/** * @var boolean */ private $all; /** * @var array */ private $roles; /** * @var array */ private $users; /** * Set the permission entities * * @param boolean $all * @param array $roles * @param array $users */ private function set_entities($all, $roles, $users) { $this->all = $all; $this->roles = $roles; $this->users = $users; }
The Constructor Function
In a first step, the constructor function initializes the $all
, $roles
and $users
variables using the set_entitites()
function. Next, it calls a private function for setting the capabilities, and another one for the Media Library restriction. These are exactly the steps we defined in our scenario.
/** * Creates a new instance of the Roles Class * * @param boolean $all * @param array $roles * @param array $users */ function __construct($all = false, $roles = array(), $users = array()) { // Set the allowed entities $this->set_entities($all, $roles, $users); // Set the user access permission $this->set_permissions(); // Media Library Filter $this->media_filter(); }
Static Functions
Static functions don't require class initialization. They are similar to independent functions, but simply linked to the specified class. I have decided to keep some functions static because they can be used independently; and you might find them useful in another context.
These functions are filter_roles()
and filter_users()
; which are associated with two other functions role_has_caps()
and user_has_caps()
. The functions play the part of a filter. They filter roles (or users) based on capabilities they have and don't have.
Both functions accept two arguments:
-
$include
Array An array of capabilities that the role has. -
$exclude
Array An array of capabilities that the role doesn't have.
/** * Filter all roles of the blog based on capabilities * * @static * @param array $include Array of capabilities to include * @param array $exclude Array of capabilities to exclude * @return array */ static function filter_roles($include, $exclude) { $filtered_roles = array(); global $wp_roles; $roles = $wp_roles->get_names(); foreach ($roles as $role_id => $role_name) { $role = get_role($role_id); if (self::role_has_caps($role, $include) && !self::role_has_caps($role, $exclude)) { $filtered_roles[] = $role_id; } } return $filtered_roles; } /** * Filter all users of the blog based on capabilities * * @static * @param array $include Array of capabilities to include * @param array $exclude Array of capabilities to exclude * @return array */ static function filter_users($include, $exclude) { $filtered_users = array(); $users = get_users(); foreach ($users as $user) { $user = new WP_User($user->ID); if (self::user_has_caps($user, $include) && !self::user_has_caps($user, $exclude)) { $filtered_users[] = $user->ID; } } return $filtered_users; }
The functions loop through all of the roles and users in the database. For each role (or user), it checks if it has the required capabilities, and doesn't have the capabilities to exclude. This check is done with the role_has_caps()
and user_has_caps()
functions.
These two functions (role_has_caps()
and user_has_caps()
) accept two arguments:
- $role (or $user) String The role ID or the user ID.
- $caps Array An array of capabilities to check against.
If the role (or user) has the specified capabilities in the $caps
array, the function returns true. In the other case, the function returns false. The function basically loops through each capability and checks that the role (or user) has the specified capability.
/** * Returns true if a role has the capabilities in the passed array * * @static * @param $role * @param $caps * @return bool */ static function role_has_caps($role, $caps) { foreach ($caps as $cap) { if (!$role->has_cap($cap)) { return false; } } return true; } /** * Returns true if a user has the capabilities in the passed array * * @static * @param $user * @param $caps * @return bool */ static function user_has_caps($user, $caps) { foreach ($caps as $cap) { if (!$user->has_cap($cap)) { return false; } } return true; }
Adding Permissions
This is the first step on imposing the law of our plugin. I have distributed the functionality over 3 functions: One for setting permissions to all users, one for setting permissions to the roles, and another for setting permissions to the specified users. The main function simply decides which functions to call.
/** * Set the Menu and Pages access permissions */ private function set_permissions() { $this->set_all_permissions(); if (!$this->all) { $this->set_roles_permissions(); $this->set_users_permissions(); } }
The set_all_permissions()
function loops through all users in the blog, and add (or remove) the "wptuts_client
" capability depending on the value of the $all
variable. We get the full list of users using the get_users()
function; and initialize a new WP_User
object to get access to the add_cap()
and remove_cap()
functions.
/** * Set the permissions for ALL users */ private function set_all_permissions() { $users = get_users(); foreach ($users as $user) { $user = new WP_User($user->ID); if ($this->all) { $user->add_cap('wptuts_client'); } else { $user->remove_cap('wptuts_client'); } } }
The set_roles_permissions()
function loops through all the roles in the blog and removes the "wptuts_client
" capability. After that, it loops through roles in the $roles
array and adds the "wptuts_client
" capability. The first step was to ensure that we clean the capability from roles which may have previously had it.
/** * Set the permissions for Roles */ private function set_roles_permissions() { global $wp_roles; $roles = $wp_roles->get_names(); foreach ($roles as $role_id => $role_name) { $role = get_role($role_id); $role->remove_cap('wptuts_client'); } if (!empty($this->roles)) { foreach ($this->roles as $role_id) { $role = get_role($role_id); $role->add_cap('wptuts_client'); } } }
The set_users_permissions()
function does the same thing as the last function. The only difference is that it targets users instead of roles.
/** * Set the permissions for specific Users */ private function set_users_permissions() { $users = get_users(); foreach ($users as $user) { $user = new WP_User($user->ID); $user->remove_cap('wptuts_client'); } if (!empty($this->users)) { foreach ($this->users as $user_id) { $user = new WP_User($user_id); $user->add_cap('wptuts_client'); } } }
Media Library Filter
Now we have set the right permissions for the right entities. (Being either a user or a role) We also have to restrict the Media Library access for the fourth category that we distinguished in the scenario.
This category of roles (or users) has the "wptuts_client
" capability, but doesn't have the "upload_files
" capability. And that's where our filter functions come into play. They'll help us filter and return this category of roles (or users).
For this category, we'll add two capabilities "upload_files
" and "remove_upload_files
". The "upload_files
" will give full access to the Media Library; and the other capability will be used to filter the Media Library posts, and it'll also be used to remove the "upload_files
" capability once the "wptuts_client
" capability is also removed.
/** * Restrict Media Access */ private function media_filter() { // Apply the media filter for current Clients $roles = self::filter_roles(array('wptuts_client'), array('upload_files')); $users = self::filter_users(array('wptuts_client'), array('upload_files')); $this->roles_add_cap($roles, 'upload_files'); $this->roles_add_cap($roles, 'remove_upload_files'); $this->users_add_cap($users, 'upload_files'); $this->users_add_cap($users, 'remove_upload_files'); // Restrict Media Library access add_filter('parse_query', array(&$this, 'restrict_media_library')); // For cleaning purposes $clean_roles = self::filter_roles(array('remove_upload_files'), array('wptuts_client')); $clean_users = self::filter_users(array('remove_upload_files'), array('wptuts_client')); $this->roles_remove_cap($clean_roles, 'upload_files'); $this->roles_remove_cap($clean_roles, 'remove_upload_files'); $this->users_remove_cap($clean_users, 'upload_files'); $this->users_remove_cap($clean_users, 'remove_upload_files'); }
After setting the capabilities to this category, we hook to the "parse_query
" filter. This filter allows us to change the posts returned by WP_Query
. In our case, we are going to set the "author
" query variable. This results in returning only posts created by the specified author.
/** * Restrict Media Library access * * @param $wp_query */ public function restrict_media_library($wp_query) { if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/upload.php')) { if (current_user_can('remove_upload_files')) { global $current_user; $wp_query->set('author', $current_user->ID); } } else if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/media-upload.php')) { if (current_user_can('remove_upload_files')) { global $current_user; $wp_query->set('author', $current_user->ID); } } }
The Full Code
if (!class_exists('wpttuts_roles')) { class wpttuts_roles { /** * Determines if all users will have the required permissions * * @var boolean */ private $all; /** * An array with the roles which have the required permissions * * @var array */ private $roles; /** * An array with the user names which have the required permissions * * @var array */ private $users; /** * Creates a new instance of the Roles Class * * @param boolean $all * @param array $roles * @param array $users */ function __construct($all = false, $roles = array(), $users = array()) { // Set the allowed entities $this->set_entities($all, $roles, $users); // Set the user access permission $this->set_permissions(); // Media Library Filter $this->media_filter(); } /** * Set the permission entities * * @param boolean $all * @param array $roles * @param array $users */ private function set_entities($all, $roles, $users) { $this->all = $all; $this->roles = $roles; $this->users = $users; } /** * Set the Menu and Pages access permissions */ private function set_permissions() { $this->set_all_permissions(); if (!$this->all) { $this->set_roles_permissions(); $this->set_users_permissions(); } } /** * Set the permissions for ALL users */ private function set_all_permissions() { $users = get_users(); foreach ($users as $user) { $user = new WP_User($user->ID); if ($this->all) { $user->add_cap('wptuts_client'); } else { $user->remove_cap('wptuts_client'); } } } /** * Set the permissions for Roles */ private function set_roles_permissions() { global $wp_roles; $roles = $wp_roles->get_names(); foreach ($roles as $role_id => $role_name) { $role = get_role($role_id); $role->remove_cap('wptuts_client'); } if (!empty($this->roles)) { foreach ($this->roles as $role_id) { $role = get_role($role_id); $role->add_cap('wptuts_client'); } } } /** * Set the permissions for specific Users */ private function set_users_permissions() { $users = get_users(); foreach ($users as $user) { $user = new WP_User($user->ID); $user->remove_cap('wptuts_client'); } if (!empty($this->users)) { foreach ($this->users as $user_id) { $user = new WP_User($user_id); $user->add_cap('wptuts_client'); } } } /** * Restrict Media Access */ private function media_filter() { // Apply the media filter for currenct AdPress Clients $roles = self::filter_roles(array('wptuts_client'), array('upload_files')); $users = self::filter_users(array('wptuts_client'), array('upload_files')); $this->roles_add_cap($roles, 'upload_files'); $this->roles_add_cap($roles, 'remove_upload_files'); $this->users_add_cap($users, 'upload_files'); $this->users_add_cap($users, 'remove_upload_files'); // Restrict Media Library access add_filter('parse_query', array(&$this, 'restrict_media_library')); // For cleaning purposes $clean_roles = self::filter_roles(array('remove_upload_files'), array('wptuts_client')); $clean_users = self::filter_users(array('remove_upload_files'), array('wptuts_client')); $this->roles_remove_cap($clean_roles, 'upload_files'); $this->roles_remove_cap($clean_roles, 'remove_upload_files'); $this->users_remove_cap($clean_users, 'upload_files'); $this->users_remove_cap($clean_users, 'remove_upload_files'); } /** * Add a capability to an Array of roles * * @param $roles * @param $cap */ private function roles_add_cap($roles, $cap) { foreach ($roles as $role) { $role = get_role($role); $role->add_cap($cap); } } /** * Add a capability to an Array of users * * @param $users * @param $cap */ private function users_add_cap($users, $cap) { foreach ($users as $user) { $user = new WP_User($user); $user->add_cap($cap); } } /** * Remove a capability from an Array of roles * * @param $roles * @param $cap */ private function roles_remove_cap($roles, $cap) { foreach ($roles as $role) { $role = get_role($role); $role->remove_cap($cap); } } /** * Remove a capability from an Array of users * * @param $users * @param $cap */ private function users_remove_cap($users, $cap) { foreach ($users as $user) { $user = new WP_User($user); $user->remove_cap($cap); } } /** * Filter all roles of the blog based on capabilities * * @static * @param array $include Array of capabilities to include * @param array $exclude Array of capabilities to exclude * @return array */ static function filter_roles($include, $exclude) { $filtered_roles = array(); global $wp_roles; $roles = $wp_roles->get_names(); foreach ($roles as $role_id => $role_name) { $role = get_role($role_id); if (self::role_has_caps($role, $include) && !self::role_has_caps($role, $exclude)) { $filtered_roles[] = $role_id; } } return $filtered_roles; } /** * Returns true if a role has the capabilities in the passed array * * @static * @param $role * @param $caps * @return bool */ static function role_has_caps($role, $caps) { foreach ($caps as $cap) { if (!$role->has_cap($cap)) { return false; } } return true; } /** * Filter all users of the blog based on capabilities * * @static * @param array $include Array of capabilities to include * @param array $exclude Array of capabilities to exclude * @return array */ static function filter_users($include, $exclude) { $filtered_users = array(); $users = get_users(); foreach ($users as $user) { $user = new WP_User($user->ID); if (self::user_has_caps($user, $include) && !self::user_has_caps($user, $exclude)) { $filtered_users[] = $user->ID; } } return $filtered_users; } /** * Returns true if a user has the capabilities in the passed array * * @static * @param $user * @param $caps * @return bool */ static function user_has_caps($user, $caps) { foreach ($caps as $cap) { if (!$user->has_cap($cap)) { return false; } } return true; } /** * Restrict Media Library access * * @param $wp_query */ public function restrict_media_library($wp_query) { if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/upload.php')) { if (current_user_can('remove_upload_files')) { global $current_user; $wp_query->set('author', $current_user->ID); } } else if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/media-upload.php')) { if (current_user_can('remove_upload_files')) { global $current_user; $wp_query->set('author', $current_user->ID); } } } } }
Conclusion
In this tutorial, I tried to use the material we learned from the previous two posts to create a custom solution for roles and capabilities. The solution was encapsulated in one class which can be customized for your own needs or plugins. You can find the code on Github.
If you have any questions, suggestions or improvements feel free to post it in the comments.
Comments