WordPress read capability in depth

WordPress read capability

read user capability

Did you ever ask yourself, what WordPress read capability really allows to read? Any unregistered visitor of your blog can see and read any public post without limitations? What the purpose of ‘read’ user capability? After making little investigation I don’t sure that WordPress read capability should be called ‘read’ but not ‘user_profile’ for example. Why I got such conclusion? Because of WordPress read capability is responsible for these WordPress admin back-end menu items only:
“Dashboard”-“Home”, “Dashboard”-“My Sites” (for multisite WP installation) and “Profile”-“Your Profile”.
Thus if you revoke ‘read’ capability from some user, she could not access to her profile then. Such user will get error message from WordPress just after login: “You do not have sufficient permissions to access this page”.

WordPress Codex page declares that:
‘read’ capability was introduced since WorPress version 2.0, it allows access to Administration Panel options: Dashboard, Users > Your Profile
and used nowhere in the core code except the menu.php. While it is absolute true about menu items, source code information is outdated a little. Let’s dive together inside WordPress core source code and look for the reason to such capability (‘read’) was implemented.

WordPress read capability at PHP files

‘read’ user capability is found at the next .php files from WordPress core source code:

Source Code

wp-login.php: The only place where ‘read’ capability is met here is block of code started from line 631:

631
632
633
634
635
636
637
638
639
640
641
	if ( ( empty( $redirect_to ) || $redirect_to == 'wp-admin/' || $redirect_to == admin_url() ) ) {
		// If the user doesn't belong to a blog, send them to user admin. If the user can't edit posts, send them to their profile.
		if ( is_multisite() && !get_active_blog_for_user($user->ID) && !is_super_admin( $user->ID ) )
			$redirect_to = user_admin_url();
		elseif ( is_multisite() && !$user->has_cap('read') )
			$redirect_to = get_dashboard_url( $user->ID );
		elseif ( !$user->has_cap('edit_posts') )
			$redirect_to = admin_url('profile.php');
	}
	wp_safe_redirect($redirect_to);
	exit();

As you can see at line 636 if user has not ‘read’ capability under multi-site environment she will be redirected to her’s dashboard. get_dashboard_url() function works this way: If a user does not belong to any site, the global user dashboard is used. If the user belongs to the current site, the dashboard for the current site is returned. If the user cannot edit the current site, the dashboard to the user’s primary blog is returned. Otherwise, that is if user is under single-site environment or user has ‘read’ capability, WordPress redirects her to her’s profile page.

wp-includes/capabilities.php: There is map_meta_cap() function in this file, which “map meta capabilities to primitive capabilities” according to source code comments. That is for our case this function maps ‘read_post’, or ‘read_page’ meta capabilities to the ‘read’ capability for standard public posts and pages (line #1133) post types or for the custom defined one (if exists) for public post of custom post type:

1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
	case 'read_post':
	case 'read_page':
		$post = get_post( $args[0] );
 
		if ( 'revision' == $post->post_type ) {
			$post = get_post( $post->post_parent );
		}
 
		$post_type = get_post_type_object( $post->post_type );
 
		if ( ! $post_type->map_meta_cap ) {
			$caps[] = $post_type->cap->$cap;
			// Prior to 3.1 we would re-call map_meta_cap here.
			if ( 'read_post' == $cap )
				$cap = $post_type->cap->$cap;
			break;
		}
 
		$status_obj = get_post_status_object( $post->post_status );
		if ( $status_obj->public ) {
			$caps[] = $post_type->cap->read;
			break;
		}

wp-includes/post.php: This file uses ‘read’ capability just in case of mapping to the same ‘read’ one for the standart ‘post’ post type, look at line 1430:

1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
	// Primitive capabilities used within map_meta_cap():
	if ( $args->map_meta_cap ) {
		$default_capabilities_for_mapping = array(
			'read'                   => 'read',
			'delete_posts'           => 'delete_'           . $plural_base,
			'delete_private_posts'   => 'delete_private_'   . $plural_base,
			'delete_published_posts' => 'delete_published_' . $plural_base,
			'delete_others_posts'    => 'delete_others_'    . $plural_base,
			'edit_private_posts'     => 'edit_private_'     . $plural_base,
			'edit_published_posts'   => 'edit_published_'   . $plural_base,
		);
		$default_capabilities = array_merge( $default_capabilities, $default_capabilities_for_mapping );
	}

But while ‘read’ capability is defined as one of the key capability to access post content, I don’t find in the code any limitation for access to the public posts for unregistered users or registered and logged-in users without ‘read’ capability. Such user lose access to the WordPress dashboard and user profile in that case only. Following investigation of wp-admin/menu.php file just confirms that.

wp-admin/menu.php: This file uses ‘read’ capability to limit access for selected menus and menu items at WordPress admin dashboard:

25
26
27
28
29
30
31
$menu[2] = array( __('Dashboard'), 'read', 'index.php', '', 'menu-top menu-top-first menu-icon-dashboard', 'menu-dashboard', 'none' );
 
$submenu[ 'index.php' ][0] = array( __('Home'), 'read', 'index.php' );
 
if ( is_multisite() ) {
	$submenu[ 'index.php' ][5] = array( __('My Sites'), 'read', 'my-sites.php' );
}

As you see above such menu items are: ‘Dashboard’, ‘Home’ and ‘My Sites’ for multi-site WordPress installation.

The next ‘read’ capability occurrence in this file is line #42 where it is used to check access to … It seems a joke but its true – line separator between different menus :).

42
  $menu[4] = array( '', 'read', 'separator1', '', 'wp-menu-separator' );

The same code could be found at line #137

137
  $menu[59] = array( '', 'read', 'separator2', '', 'wp-menu-separator' );

This shows us the real level of responsibility for the ‘read’ user capability. It could be available for any registered WordPress user. The only case when you may to wish revoke this capability when you plan to prohibit users access to their user profile. Look at the code below. Lines 181, 191 and 194 are the subject of our interest here:

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
if ( current_user_can('list_users') )
	$menu[70] = array( __('Users'), 'list_users', 'users.php', '', 'menu-top menu-icon-users', 'menu-users', 'none' );
else
	$menu[70] = array( __('Profile'), 'read', 'profile.php', '', 'menu-top menu-icon-users', 'menu-users', 'none' );
 
if ( current_user_can('list_users') ) {
	$_wp_real_parent_file['profile.php'] = 'users.php'; // Back-compat for plugins adding submenus to profile.php.
	$submenu['users.php'][5] = array(__('All Users'), 'list_users', 'users.php');
	if ( current_user_can('create_users') )
		$submenu['users.php'][10] = array(_x('Add New', 'user'), 'create_users', 'user-new.php');
	else
		$submenu['users.php'][10] = array(_x('Add New', 'user'), 'promote_users', 'user-new.php');
 
	$submenu['users.php'][15] = array(__('Your Profile'), 'read', 'profile.php');
} else {
	$_wp_real_parent_file['users.php'] = 'profile.php';
	$submenu['profile.php'][5] = array(__('Your Profile'), 'read', 'profile.php');
	if ( current_user_can('create_users') )
		$submenu['profile.php'][10] = array(__('Add New User'), 'create_users', 'user-new.php');
	else
		$submenu['profile.php'][10] = array(__('Add New User'), 'promote_users', 'user-new.php');
}

Just remember that user could not change her password manually in case she has not access to her user profile.

wp-admin/network/menu.php: This file uses ‘read’ capability to decide show or not separator at the Network admin menu:

13
  $menu[4] = array( '', 'read', 'separator1', '', 'wp-menu-separator' );
69
  $menu[99] = array( '', 'read', 'separator-last', '', 'wp-menu-separator-last' );

Thus, it doesn’t fulfill any security function here.

wp-admin/mysites.php: This page shows an individual user all of his or her sites in this network, and also allows that user to set a primary site. He or she can use the links under each site to visit either the frontend or the dashboard for that site. If user has not ‘read’ capability access to this page is prohibited.

15
16
if ( ! current_user_can('read') )
  wp_die( __( 'You do not have sufficient permissions to view this page.' ) );

wp-admin/includes/schema.php: This file contains code to create ‘read’ capability. At line 569 you can find the populate_roles_160() function:

564
565
566
567
568
569
/**
 * Create the roles for WordPress 2.0
 *
 * @since 2.0.0
 */
function populate_roles_160() {

with this line of code:

610
$role->add_cap('read');

which introduced the ‘read’ user capability together with others 1st time since WordPress version 2.0.

This article uses WordPress 3.5 RC2 source code.