get_posts() not working when accessing with a custom user role

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:

get_posts(array(
    '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]);
    if(ctype_digit($args[2])
    && $pt !== FALSE
    ) {
        $pt_obj = get_post_type_object($pt);
        if(!is_null($pt_obj)
        && !$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);