I've stumbled across something that I can't seem to find any documentation on regarding WP 'get_posts' function.

I've got a web application that has multiple custom post-types and multiple custom user roles. They're all necessary for an assortment of reasons. Different users types within the organization need to see different things and be able to edit different aspects of the custom post-types.

In the custom post-type edit screens, I have metaboxes that display user provided data, however, users with backend access, using different custom user roles, need to be able to correct or modify the data that users have supplied or to manually create an entry.

I use the following 'get_posts()' method to generate dropdowns based on entries from other custom post-types:

<select name="mwss_session">
    <option value="">Please select a Session</option>
        $get_sessions = get_posts( array(
            'post_type'         => 'seasonal_sessions',
            'post_status'       => 'publish',
            'posts_per_page'    => -1,
            'meta_key'          => 'mwss_session_status',
            'meta_value'        => 'true',
            'meta_compare'      => '='
        ) );
        $selected_session = $mwss_session;
        foreach( $get_sessions as $active_session ) {
            $session_name = get_the_title( $active_session->ID );
            echo '<option value="' . $session_name . '"';
            if( $selected_session === $session_name ){ echo 'selected'; }
            echo '>' . $session_name . '</option>';

It works as you'd expect it to. You visit the edit screen and the dropdown is populated with 'Sessions' whose 'status' is set to 'true'. This same get_posts() method works on the front end as well, logged in or not.

However, when visiting the edit.php screen for this post type using one of the custom user roles I created, this dropdown and others using the same method, all fail to return results.

Here's an example of one of the custom user roles capabilities:

$result = add_role(
    'mwss_sitesuper', __('Site Supervisor'),
        //WordPress Capabilities
        'level_9'                       => false,
        'level_8'                       => true,
        'level_7'                       => true,
        'level_6'                       => true,
        'level_5'                       => true,
        'level_4'                       => true,
        'level_3'                       => true,
        'level_2'                       => true,
        'level_1'                       => true,
        'level_0'                       => true,
        'read'                          => true,
        'read_private_pages'            => true,
        'read_private_posts'            => true,
        'create_posts'                  => true,
        'publish_posts'                 => true,
        'edit_users'                    => true,
        'edit_posts'                    => true,
        'edit_pages'                    => true,
        'edit_published_posts'          => true,
        'edit_published_pages'          => true,
        'edit_private_pages'            => true,
        'edit_private_posts'            => true,
        'edit_others_posts'             => true,
        'edit_others_pages'             => true,
        'publish_posts'                 => true,
        'publish_pages'                 => true,
        'delete_posts'                  => true,
        'delete_pages'                  => true,
        'delete_private_pages'          => true,
        'delete_private_posts'          => true,
        'delete_published_pages'        => true,
        'delete_published_posts'        => true,
        'delete_others_posts'           => true,
        'delete_others_pages'           => true,
        'manage_options'                => false,
        'manage_categories'             => false,
        'manage_links'                  => false,
        'moderate_comments'             => true,
        'unfiltered_html'               => false,
        'upload_files'                  => false,
        'export'                        => false,
        'import'                        => false,
        'list_users'                    => true,
        'edit_themes'                   => false,
        'install_plugins'               => false,
        'update_plugin'                 => false,
        'update_core'                   => false

Any idea why get_posts() only works with Administrator or Editor roles in the back end, but works for all user roles on the front end?

Update: After delving pretty deeply into capabilities, I've come to the conclusion that this is something unrelated. On the front end, regardless of the user's capabilities or role, the same 'get_posts' function is returning results. So if a user role as low as subscriber, or if all of the custom user roles are able to view the dropdown on the front end populated with the expected 'sessions', they then theoretically have the capability to do so, right? Or am I missing something? The issue appears to be isolated to metaboxes within CPT. If I use WP_Query, it works, but WP_Query will disrupt the rest of the admin loop.

  • A sidenote but your querying for mwss_session_status is super expensive/slow, mwss_session_status should really have been a taxonomy with 2 terms, 'true` and false, it could be 100x faster to query – Tom J Nowell Nov 21 '18 at 18:02
  • I hear you @tomjnowell, that was my original plan but there's a lot more data in the sessions than just the status and it wouldn't have worked as a taxonomy. For this purpose however I only need to check the status. At any given time there shouldn't be more than 4 or 5 sessions in the system, and usually no more than 2 active. So it's a really quick query. – Tony Djukic Nov 21 '18 at 18:08
  • You don't have to put everything in taxonomies, storing the status by assigning terms doesn't stop you using post meta for other fields – Tom J Nowell Nov 21 '18 at 22:15

I have this problem as well, @tony-djukic. I have an administrator user that can see two posts of a custom post type. I have two subscriber users, each are able to only see the opposite post as the other. Here's what I'm using to (apparently not) get all the posts:

    'numberposts'       => -1,
    'post_type'         => 'my-cpt',
    'post_status'       => get_post_stati(),

After searching for several of hours in each of the past couple of days for a solution, I came across your (1.5-year-old) unanswered question. With no other option, I traced the code. Here's my work...

1.) According to the documentation, the get_posts() method returns the result of WP_Query->query() with an array of arguments passed (which is of no importance when considering the issue at hand).

2.) The documentation says that the WP_Query->query() method is a pretty simple wrapper that initializes the WP_Query object before returning the results of WP_Query->get_posts().

3.) Next, the WP_Query->get_posts() method is called. Somewhere around line 2434, the read_post and edit_post capabilities come into play. Mind you, this is not the read_CPT or edit_CPT primitives of the custom post type, but rather the primitives for the post type. Those non-altered primitives are then checked in a current_user_can call (see lines 3087, 3096, 3105, and 3111). In other words, if your user does not have the read_post and edit_post capabilities, the posts checked in the current_user_can() call will not be included in the results. This is quite counter to the purpose of custom capabilities: the capabilities checked should be the read_CPT or edit_CPT capabilities so this is quite obviously some kind of accident on the coder's part. After all, granting these permission will likely allow your user to edit posts (as well as your custom post type).

NOTE: The reason why the user is able to read on the front-end is more because of the public=TRUE given when registering the post type, not necessarily because of the capability given to the user. What I think I see in the code is that the higher-level capabilities play a role in protected and private posts more than they do for those CPT's defined as being registered as public.

LONG-TERM FIX: WordPress should define these two variables (on lines 2434 and 2435) after performing a check to determine if a CPT is being queried: obviously, this was overlooked in the coding. What should happen is these variables representing capabilities should be set in the succeeding checks occurring on lines 2437-2443. Namely, that code block should be:

if ( ! empty( $post_type_object ) ) {
    $edit_cap = $post_type_object->cap->edit_post;
    $read_cap = $post_type_object->cap->read_post;
    $edit_others_cap  = $post_type_object->cap->edit_others_posts;
    $read_private_cap = $post_type_object->cap->read_private_posts;
} else {
    $edit_cap = 'edit_'. $post_type_cap;
    $read_cap = 'read_'. $post_type_cap;
    $edit_others_cap  = 'edit_others_' . $post_type_cap . 's';
    $read_private_cap = 'read_private_' . $post_type_cap . 's';

SHORT-TERM FIX: After all that, I came up with the following temporary solution. The user_has_cap filter can be implemented in an early hook. The ID of the object to the hook is passed in $args[2] of the hook, which can be checked to see if it is the ID of an object having a custom post type. If it is, then the result of the user_can method on that custom capability can be returned. The whole thing could look something like this:

add_filter('user_has_cap' function($allcaps, $caps, $args, $user){
    $pt = get_post_type($args[2]);
    && $pt !== FALSE
    ) {
        $pt_obj = get_post_type_object($pt);
        && !$pt_obj->_builtin
        ) {
            foreach($caps as $cap) {
                $type = substr($cap, strrpos($cap, '_')+1);
                if($type === 'post'
                || $type === 'posts'
                ) {
                    // $pt_obj->cap set by get_post_type_capabilities()
                    $allcaps[$cap] = user_can($user, $pt_obj->cap->$cap, $args);
    return $allcaps;
}, 0, 4);

Try adding read_seasonal_sessions to your capabilities array and setting that to true. Each new CPT gets 7 new capabilities to add to user roles. (see capability_type on register_post_type())


Add map_meta_cap to your register_post_type function (if you have it) and set it to true. (see map_meta_cap under register_post_type())

By default, map_meta_cap is null, so I don't think it's mapping the post capabilities directly to your CPT.

  • Thanks for this @socki03... ...didn't seem to change anything. I added the capabilities to each user role, then output a print_r of the capabilities to confirm they were there, but that didn't change the results. Next, I added 'map_meta_cap' => true, but that didn't have any effect. And finally, I added capabilities to post_type because I just want them to behave like regular posts, but that didn't work. I have capability_type set to 'seasonal_session'. Then added capabilities as follows: 'capabilities' => array('read_seasonal_session', 'read_seasonal_sessions'), – Tony Djukic Nov 21 '18 at 19:46
  • Update: I've installed several capability management plugins and all of the custom post-types have all of the requisite capabilities enabled now, however, the end result is still the same and the get_posts() still returns no results. So frustrating. – Tony Djukic Nov 21 '18 at 20:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.