wordpress, blog, blogger

As far as user security is concerned, WordPress relies on its concept of roles and capabilities. While roles are pretty straightforward, capabilities are a bit more complicated. That is because, for example, the permission to edit a post depends not only on the user bearing the capability, but also the owner of the post. To address this issue, WordPress introduces the concept of meta capabilities. This article is a tutorial for mapping meta capabilities to primitive ones.

What is a meta capability

A meta capability is a capability that can be mapped to one or more primitive ones, possibly depending on other factors. For example, when a user with the edit_posts capability tries to edit a post, they must either be the owner of the post or have the edit_others_posts capability. That makes edit_posts a meta capability which may eventually be mapped to the edit_others_posts one. Of course, this is an oversimplification for example’s sake. In reality, there are many more conditions to check before we grant edit access.

The basics

WordPress maps its default meta capabilities with the map_meta_cap() function. When this function has done its job, it applies the map_meta_cap filter to give plugins the opportunity to further process the produced primitive capabilities. The resulting array must contain the final capabilities a user has to have in order to be given access. This implies two things:

An empty array will grant access: An empty array means that there are no primitive capabilities to check in order to grant access. Therefore, we have no reason to revoke it.

Non-existent capabilities will revoke access: If the array contains a capability that the user does not have, the check will obviously fail and access will be revoked. Consequently, adding a completely made up capability to the array would be a technique to disallow access. The problem though would be that other plugins may define the capability we used and break our code.

Which brings us to the do_not_allow pseudo capability. WordPress has reserved this capability for the above reason, making sure it cannot be defined as a real capability. All we have to do, in order to deny access, is include this capability in the result array.

Another thing to note is that Super Admins are always granted access, except when do_not_allow is returned. In other words, returning this pseudo capability is the only way to deny access to everyone.

Meta capability mapping at work

For our first example, let’s say that we want to restrict the upload_files capability. We will only allow it if the site’s upload directory size is below a certain threshold:

define('UPLOAD_DIR_THRESHOLD',/* some number */);

function tutorial_upload_dir_size() {
  // calculate $size
  return $size;
}

function tutorial_map_meta_cap($caps,$cap) {
  $size=tutorial_upload_dir_size();
  if(('upload_files'==$cap)&&($size>=UPLOAD_DIR_THRESHOLD)) 
    $caps[]='do_not_allow';
  return $caps;
}

add_filter('map_meta_cap','tutorial_map_meta_cap',10,2);
PHP

Notice that we don’t take any special action if $size<UPLOAD_DIR_THRESHOLD. That’s because this is a known capability which WordPress has already filtered and included in $caps. Which means that it will be further checked against the user’s capabilities.

Virtual capabilities

Another way to use meta capability mapping, in fact the intended one, is to map virtual capabilities. For example, let’s assume we have implemented a credits system of sorts. Moreover, there is a page in our admin interface that we want to be accessible by any user with 1000 or more credits, as well as admins with 200 or more. The way to go about this is something like the following:

function tutorial_admin_page_render() { ?>

    <!-- admin page html -->

<?php }

function tutorial_admin_menu() {
  add_menu_page(
  	__('Tutorial Admin Page','tutorial'),__('Tutorial','tutorial'),
  	'show_tutorial_admin_screen','tutorial-menu',
  	'tutorial_admin_page_render',
  	'dashicons-paperclip'
  );
}

add_action('admin_menu','tutorial_admin_menu');

function get_user_credits($uid) {
  // get user credits in $credits
  return $credits;
}

function tutorial_map_meta_cap($caps,$cap,$uid) {
  if('show_tutorial_admin_screen'==$cap) {
    $credits=get_user_credits($uid);
    if($credits<200) $caps[]='do_not_allow';
    elseif($credits<1000) $caps[]='manage_options';
    else $caps=[];
  }
  return $caps;
}

add_filter('map_meta_cap','tutorial_map_meta_cap',10,3);
PHP

What we are doing here is associating the admin page with the show_tutorial_admin_screen meta capability. When the time comes for WordPress to decide if the user has this capability, our filter handler will check their credits. Access will be denied if the user has less than 200 credits and granted if they have 1000 credits or more. In all other cases, the condition will default to whether the user is an admin (has the manage_options capability).