Advertisement
  1. Code
  2. Creative Coding

WordPress Roles and Capabilities: A Real Life Example

Scroll to top
Read Time: 18 min

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.

1
2
/*

3
  Plugin Name: WordPress Roles Plugin

4
  Plugin URI: https://github.com/omarabid/WordPress-Roles-Plugin

5
  Description: A WordPress Roles Plugin

6
  Author: Abid Omar

7
  Author URI: https://omarabid.com

8
  Version: 1.0

9
 */
10
11
// Add an Admin user menu to the WordPress Dashboard

12
add_action('admin_menu', 'wptuts_admin_menu');
13
function wptuts_admin_menu() {
14
	add_menu_page('Admin Access', 'Admin Access', 'manage_options', 'wptuts-admin', 'wptuts_admin_page');
15
}
16
17
function wptuts_admin_page() {
18
	echo 'Admin Page';
19
}
20
21
// Add a client user menu to the WordPress Dashboard

22
add_action('admin_menu', 'wptuts_client_menu');
23
function wptuts_client_menu() {
24
	add_menu_page('Client Access', 'Client Access', 'wptuts_client', 'wptuts-client', 'wptuts_client_page');
25
}
26
27
function wptuts_client_page() {
28
	echo 'Client Page';
29
}

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.
1
2
// Loads and initialize the roles class

3
add_action('init', 'wptuts_start_plugin');
4
function wptuts_start_plugin() {
5
	require_once('roles_class.php');
6
	$all = false;
7
	$roles = array('subscriber');
8
	$users = array(3);
9
	new wpttuts_roles($all, $roles, $users);
10
}

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.

1
2
/**

3
 * @var boolean

4
 */
5
private $all;
6
7
/**

8
 * @var array

9
 */
10
private $roles;
11
12
/**

13
 * @var array

14
 */
15
private $users;
16
17
/**

18
 * Set the permission entities

19
 *

20
 * @param boolean $all

21
 * @param array $roles

22
 * @param array $users

23
 */
24
private function set_entities($all, $roles, $users) {
25
	$this->all = $all;
26
	$this->roles = $roles;
27
	$this->users = $users;
28
}

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.

1
2
/**

3
 * Creates a new instance of the Roles Class

4
 *

5
 * @param boolean $all

6
 * @param array $roles

7
 * @param array $users

8
 */
9
function __construct($all = false, $roles = array(), $users = array()) {
10
	// Set the allowed entities

11
	$this->set_entities($all, $roles, $users);
12
13
	// Set the user access permission

14
	$this->set_permissions();
15
16
	// Media Library Filter

17
	$this->media_filter();
18
}

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.
1
2
/**

3
 * Filter all roles of the blog based on capabilities

4
 *

5
 * @static

6
 * @param array $include Array of capabilities to include

7
 * @param array $exclude Array of capabilities to exclude

8
 * @return array

9
 */
10
static function filter_roles($include, $exclude) {
11
	$filtered_roles = array();
12
	global $wp_roles;
13
	$roles = $wp_roles->get_names();
14
	foreach ($roles as $role_id => $role_name) {
15
		$role = get_role($role_id);
16
		if (self::role_has_caps($role, $include) && !self::role_has_caps($role, $exclude)) {
17
			$filtered_roles[] = $role_id;
18
		}
19
	}
20
	return $filtered_roles;
21
}
22
23
/**

24
 * Filter all users of the blog based on capabilities

25
 *

26
 * @static

27
 * @param array $include Array of capabilities to include

28
 * @param array $exclude Array of capabilities to exclude

29
 * @return array

30
 */
31
static function filter_users($include, $exclude) {
32
	$filtered_users = array();
33
	$users = get_users();
34
	foreach ($users as $user) {
35
		$user = new WP_User($user->ID);
36
		if (self::user_has_caps($user, $include) && !self::user_has_caps($user, $exclude)) {
37
			$filtered_users[] = $user->ID;
38
		}
39
	}
40
	return $filtered_users;
41
}

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.

1
2
/**

3
 * Returns true if a role has the capabilities in the passed array

4
 *

5
 * @static

6
 * @param $role

7
 * @param $caps

8
 * @return bool

9
 */
10
static function role_has_caps($role, $caps) {
11
	foreach ($caps as $cap) {
12
		if (!$role->has_cap($cap)) {
13
			return false;
14
		}
15
	}
16
	return true;
17
}
18
19
/**

20
 * Returns true if a user has the capabilities in the passed array

21
 *

22
 * @static

23
 * @param $user

24
 * @param $caps

25
 * @return bool

26
 */
27
static function user_has_caps($user, $caps) {
28
	foreach ($caps as $cap) {
29
		if (!$user->has_cap($cap)) {
30
			return false;
31
		}
32
	}
33
	return true;
34
}

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.

1
2
/**

3
 * Set the Menu and Pages access permissions

4
 */
5
private function set_permissions() {
6
	$this->set_all_permissions();
7
	if (!$this->all) {
8
		$this->set_roles_permissions();
9
		$this->set_users_permissions();
10
	}
11
}

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.

1
2
/**

3
 * Set the permissions for ALL users

4
 */
5
private function set_all_permissions() {
6
	$users = get_users();
7
	foreach ($users as $user) {
8
		$user = new WP_User($user->ID);
9
		if ($this->all) {
10
			$user->add_cap('wptuts_client');
11
		}
12
		else {
13
			$user->remove_cap('wptuts_client');
14
		}
15
	}
16
}

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.

1
2
/**

3
 * Set the permissions for Roles

4
 */
5
private function set_roles_permissions() {
6
	global $wp_roles;
7
	$roles = $wp_roles->get_names();
8
	foreach ($roles as $role_id => $role_name) {
9
		$role = get_role($role_id);
10
		$role->remove_cap('wptuts_client');
11
	}
12
	if (!empty($this->roles)) {
13
		foreach ($this->roles as $role_id) {
14
			$role = get_role($role_id);
15
			$role->add_cap('wptuts_client');
16
		}
17
	}
18
}

The set_users_permissions() function does the same thing as the last function. The only difference is that it targets users instead of roles.

1
2
/**

3
 * Set the permissions for specific Users

4
 */
5
private function set_users_permissions() {
6
	$users = get_users();
7
	foreach ($users as $user) {
8
		$user = new WP_User($user->ID);
9
		$user->remove_cap('wptuts_client');
10
	}
11
	if (!empty($this->users)) {
12
		foreach ($this->users as $user_id) {
13
			$user = new WP_User($user_id);
14
			$user->add_cap('wptuts_client');
15
		}
16
	}
17
}

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.

1
2
/**

3
 * Restrict Media Access

4
 */
5
private function media_filter() {
6
	// Apply the media filter for current Clients

7
	$roles = self::filter_roles(array('wptuts_client'), array('upload_files'));
8
	$users = self::filter_users(array('wptuts_client'), array('upload_files'));
9
	$this->roles_add_cap($roles, 'upload_files');
10
	$this->roles_add_cap($roles, 'remove_upload_files');
11
	$this->users_add_cap($users, 'upload_files');
12
	$this->users_add_cap($users, 'remove_upload_files');
13
14
	// Restrict Media Library access

15
	add_filter('parse_query', array(&$this, 'restrict_media_library'));
16
17
	// For cleaning purposes

18
	$clean_roles = self::filter_roles(array('remove_upload_files'), array('wptuts_client'));
19
	$clean_users = self::filter_users(array('remove_upload_files'), array('wptuts_client'));
20
	$this->roles_remove_cap($clean_roles, 'upload_files');
21
	$this->roles_remove_cap($clean_roles, 'remove_upload_files');
22
	$this->users_remove_cap($clean_users, 'upload_files');
23
	$this->users_remove_cap($clean_users, 'remove_upload_files');
24
}

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.

1
2
/**

3
 * Restrict Media Library access

4
 *

5
 * @param $wp_query

6
 */
7
public function restrict_media_library($wp_query) {
8
	if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/upload.php')) {
9
		if (current_user_can('remove_upload_files')) {
10
			global $current_user;
11
			$wp_query->set('author', $current_user->ID);
12
		}
13
	}
14
	else if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/media-upload.php')) {
15
		if (current_user_can('remove_upload_files')) {
16
			global $current_user;
17
			$wp_query->set('author', $current_user->ID);
18
		}
19
	}
20
}

The Full Code

1
2
if (!class_exists('wpttuts_roles')) {
3
	class wpttuts_roles {
4
		/**

5
		 * Determines if all users will have the required permissions

6
		 *

7
		 * @var boolean

8
		 */
9
		private $all;
10
11
		/**

12
		 * An array with the roles which have the required permissions

13
		 *

14
		 * @var array

15
		 */
16
		private $roles;
17
18
		/**

19
		 * An array with the user names which have the required permissions

20
		 *

21
		 * @var array

22
		 */
23
		private $users;
24
25
		/**

26
		 * Creates a new instance of the Roles Class 

27
		 *

28
		 * @param boolean $all

29
		 * @param array $roles

30
		 * @param array $users

31
		 */
32
		function __construct($all = false, $roles = array(), $users = array()) {
33
			// Set the allowed entities

34
			$this->set_entities($all, $roles, $users);
35
36
			// Set the user access permission

37
			$this->set_permissions();
38
39
			// Media Library Filter

40
			$this->media_filter();
41
		}
42
43
		/**

44
		 * Set the permission entities

45
		 *

46
		 * @param boolean $all

47
		 * @param array $roles

48
		 * @param array $users

49
		 */
50
		private function set_entities($all, $roles, $users) {
51
			$this->all = $all;
52
			$this->roles = $roles;
53
			$this->users = $users;
54
		}
55
56
		/**

57
		 * Set the Menu and Pages access permissions

58
		 */
59
		private function set_permissions() {
60
			$this->set_all_permissions();
61
			if (!$this->all) {
62
				$this->set_roles_permissions();
63
				$this->set_users_permissions();
64
			}
65
		}
66
67
		/**

68
		 * Set the permissions for ALL users

69
		 */
70
		private function set_all_permissions() {
71
			$users = get_users();
72
			foreach ($users as $user) {
73
				$user = new WP_User($user->ID);
74
				if ($this->all) {
75
					$user->add_cap('wptuts_client');
76
				}
77
				else {
78
					$user->remove_cap('wptuts_client');
79
				}
80
			}
81
		}
82
83
		/**

84
		 * Set the permissions for Roles

85
		 */
86
		private function set_roles_permissions() {
87
			global $wp_roles;
88
			$roles = $wp_roles->get_names();
89
			foreach ($roles as $role_id => $role_name) {
90
				$role = get_role($role_id);
91
				$role->remove_cap('wptuts_client');
92
			}
93
			if (!empty($this->roles)) {
94
				foreach ($this->roles as $role_id) {
95
					$role = get_role($role_id);
96
					$role->add_cap('wptuts_client');
97
				}
98
			}
99
		}
100
101
		/**

102
		 * Set the permissions for specific Users

103
		 */
104
		private function set_users_permissions() {
105
			$users = get_users();
106
			foreach ($users as $user) {
107
				$user = new WP_User($user->ID);
108
				$user->remove_cap('wptuts_client');
109
			}
110
			if (!empty($this->users)) {
111
				foreach ($this->users as $user_id) {
112
					$user = new WP_User($user_id);
113
					$user->add_cap('wptuts_client');
114
				}
115
			}
116
		}
117
118
		/**

119
		 * Restrict Media Access

120
		 */
121
		private function media_filter() {
122
			// Apply the media filter for currenct AdPress Clients

123
			$roles = self::filter_roles(array('wptuts_client'), array('upload_files'));
124
			$users = self::filter_users(array('wptuts_client'), array('upload_files'));
125
			$this->roles_add_cap($roles, 'upload_files');
126
			$this->roles_add_cap($roles, 'remove_upload_files');
127
			$this->users_add_cap($users, 'upload_files');
128
			$this->users_add_cap($users, 'remove_upload_files');
129
130
			// Restrict Media Library access

131
			add_filter('parse_query', array(&$this, 'restrict_media_library'));
132
133
			// For cleaning purposes

134
			$clean_roles = self::filter_roles(array('remove_upload_files'), array('wptuts_client'));
135
			$clean_users = self::filter_users(array('remove_upload_files'), array('wptuts_client'));
136
			$this->roles_remove_cap($clean_roles, 'upload_files');
137
			$this->roles_remove_cap($clean_roles, 'remove_upload_files');
138
			$this->users_remove_cap($clean_users, 'upload_files');
139
			$this->users_remove_cap($clean_users, 'remove_upload_files');
140
		}
141
142
		/**

143
		 * Add a capability to an Array of roles

144
		 *

145
		 * @param $roles

146
		 * @param $cap

147
		 */
148
		private function roles_add_cap($roles, $cap) {
149
			foreach ($roles as $role) {
150
				$role = get_role($role);
151
				$role->add_cap($cap);
152
			}
153
		}
154
155
		/**

156
		 * Add a capability to an Array of users

157
		 *

158
		 * @param $users

159
		 * @param $cap

160
		 */
161
		private function users_add_cap($users, $cap) {
162
			foreach ($users as $user) {
163
				$user = new WP_User($user);
164
				$user->add_cap($cap);
165
			}
166
		}
167
168
		/**

169
		 * Remove a capability from an Array of roles

170
		 *

171
		 * @param $roles

172
		 * @param $cap

173
		 */
174
		private function roles_remove_cap($roles, $cap) {
175
			foreach ($roles as $role) {
176
				$role = get_role($role);
177
				$role->remove_cap($cap);
178
			}
179
		}
180
181
		/**

182
		 * Remove a capability from an Array of users

183
		 *

184
		 * @param $users

185
		 * @param $cap

186
		 */
187
		private function users_remove_cap($users, $cap) {
188
			foreach ($users as $user) {
189
				$user = new WP_User($user);
190
				$user->remove_cap($cap);
191
			}
192
		}
193
194
		/**

195
		 * Filter all roles of the blog based on capabilities

196
		 *

197
		 * @static

198
		 * @param array $include Array of capabilities to include

199
		 * @param array $exclude Array of capabilities to exclude

200
		 * @return array

201
		 */
202
		static function filter_roles($include, $exclude) {
203
			$filtered_roles = array();
204
			global $wp_roles;
205
			$roles = $wp_roles->get_names();
206
			foreach ($roles as $role_id => $role_name) {
207
				$role = get_role($role_id);
208
				if (self::role_has_caps($role, $include) && !self::role_has_caps($role, $exclude)) {
209
					$filtered_roles[] = $role_id;
210
				}
211
			}
212
			return $filtered_roles;
213
		}
214
215
		/**

216
		 * Returns true if a role has the capabilities in the passed array

217
		 *

218
		 * @static

219
		 * @param $role

220
		 * @param $caps

221
		 * @return bool

222
		 */
223
		static function role_has_caps($role, $caps) {
224
			foreach ($caps as $cap) {
225
				if (!$role->has_cap($cap)) {
226
					return false;
227
				}
228
			}
229
			return true;
230
		}
231
232
		/**

233
		 * Filter all users of the blog based on capabilities

234
		 *

235
		 * @static

236
		 * @param array $include Array of capabilities to include

237
		 * @param array $exclude Array of capabilities to exclude

238
		 * @return array

239
		 */
240
		static function filter_users($include, $exclude) {
241
			$filtered_users = array();
242
			$users = get_users();
243
			foreach ($users as $user) {
244
				$user = new WP_User($user->ID);
245
				if (self::user_has_caps($user, $include) && !self::user_has_caps($user, $exclude)) {
246
					$filtered_users[] = $user->ID;
247
				}
248
			}
249
			return $filtered_users;
250
		}
251
252
		/**

253
		 * Returns true if a user has the capabilities in the passed array

254
		 *

255
		 * @static

256
		 * @param $user

257
		 * @param $caps

258
		 * @return bool

259
		 */
260
		static function user_has_caps($user, $caps) {
261
			foreach ($caps as $cap) {
262
				if (!$user->has_cap($cap)) {
263
					return false;
264
				}
265
			}
266
			return true;
267
		}
268
269
		/**

270
		 * Restrict Media Library access

271
		 *

272
		 * @param $wp_query

273
		 */
274
		public function restrict_media_library($wp_query) {
275
			if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/upload.php')) {
276
				if (current_user_can('remove_upload_files')) {
277
					global $current_user;
278
					$wp_query->set('author', $current_user->ID);
279
				}
280
			}
281
			else if (strpos($_SERVER['REQUEST_URI'], '/wp-admin/media-upload.php')) {
282
				if (current_user_can('remove_upload_files')) {
283
					global $current_user;
284
					$wp_query->set('author', $current_user->ID);
285
				}
286
			}
287
		}
288
289
	}
290
}

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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.