wordpresswordpress-capabilitieswordpress-roles

WordPress user with custom role cannot view list page for custom post types without the "create_posts" capabililty


I am running a WordPress 5.2.3 site and having trouble with something in the admin panel.

I have a custom role, let's call it librarian, and a custom post type, let's call it book.

I want to make it so that a librarian can edit a book but not create a new one.

Following the advice in another question (WordPress: Disable “Add New” on Custom Post Type) and WordPress documentation, I have ended up with this code:

// Custom post type.
register_post_type('book',
    array(
        'labels'                => array(
            'name' => __( 'book' ),
            'singular_name' => __( 'Book' )
        ),
        'capability_type'       => array('book', 'books'),
        'capabilities'          => array(
            'create_posts' => 'do_not_allow' // <-- The important bit.
        ),
        'map_meta_cap'          => true,
        'description'           => 'Book full of pages',
        'exclude_from_search'   => true,
        'publicly_queryable'    => false,
        'show_in_nav_menus'     => false,
        'show_ui'               => true,
        'show_in_menu'          => true,
        'show_in_rest'          => true,
        'menu_icon'             => 'dashicons-location',
        'menu_position'         => 5,
        'supports'              => array('title', 'revisions')
    ));
// Custom role.
add_role('librarian', 'Librarian', array(
    'read'                  => true,
    'edit_books'            => true,
    'edit_published_books'  => true
));

I was expecting that when I visited edit.php?post_type=book as a librariranthen I would see the list of books for editing, but I would not see the Add New button. However, what I actually get is a 403 response:

Sorry, you are not allowed to access this page.

I think this may be a bug in WordPress, because of the following cases:

These make me think that it isn't a problem with the custom post type set up in general.

This makes me think that it isn't a problem with the custom role set up in general.

Has anyone encountered this issue before? Have I missed anything from my configuration? Or is there an easy patch or workaround?

Any help would be appreciated! Thanks.


Solution

  • It appears that this is a bug in WordPress. I have found the source of the problem and a workaround.

    Workaround

    If you're not interested in the cause, the workaround is to comment out this bit of cosmetic code in wp-admin/includes/menu.php:

    https://github.com/WordPress/WordPress/blob/master/wp-admin/includes/menu.php#L168

    /*
     * If there is only one submenu and it is has same destination as the parent,
     * remove the submenu.
     */
    if ( ! empty( $submenu[ $data[2] ] ) && 1 == count( $submenu[ $data[2] ] ) ) {
        $subs      = $submenu[ $data[2] ];
        $first_sub = reset( $subs );
        if ( $data[2] == $first_sub[2] ) {
            unset( $submenu[ $data[2] ] );
        }
    }
    

    This will mean that some menu items that previously didn't show a submenu now will (with a single item the same as the main menu item), but that is only a cosmetic UI change.

    Cause

    For those of you that want to know the detail…

    Accessing edit.php?post_type=book was failing this check in wp-admin/includes/menu.php:

    https://github.com/WordPress/WordPress/blob/master/wp-admin/includes/menu.php#L341

    if ( ! user_can_access_admin_page() ) {
    
        /**
         * Fires when access to an admin page is denied.
         *
         * @since 2.5.0
         */
        do_action( 'admin_page_access_denied' );
    
        wp_die( __( 'Sorry, you are not allowed to access this page.' ), 403 );
    }
    

    The call to user_can_access_admin_page() calls through to get_admin_page_parent().

    If the submenu has been removed, get_admin_page_parent() returns an empty parent which ultimately causes user_can_access_admin_page() to erroneously return false in the case of the librarian role (the administrator role passes for a different reason).

    If the submenu is left in place, get_admin_page_parent() returns a non-empty parent and the access check proceeds correctly from there.

    So the root issue is that the global $submenu is being used to both determine the UI and also to make decisions on the permissions hierarchy. I don't see an immediate quick fix for this problem that wouldn't have side effects elsewhere throughout the WordPress code, other than the workaround above.