diff --git a/wp-admin/includes/class-wp-posts-list-table.php b/wp-admin/includes/class-wp-posts-list-table.php index 19686959e2..213ca201d9 100644 --- a/wp-admin/includes/class-wp-posts-list-table.php +++ b/wp-admin/includes/class-wp-posts-list-table.php @@ -1660,7 +1660,7 @@ class WP_Posts_List_Table extends WP_List_Table { if ( current_user_can( $post_type_object->cap->edit_others_posts ) ) { $users_opt = array( 'hide_if_only_one_author' => false, - 'who' => 'authors', + 'capability' => array( $post_type_object->cap->edit_posts ), 'name' => 'post_author', 'class' => 'authors', 'multi' => 1, diff --git a/wp-admin/includes/meta-boxes.php b/wp-admin/includes/meta-boxes.php index 193877ce96..c14ae52ddc 100644 --- a/wp-admin/includes/meta-boxes.php +++ b/wp-admin/includes/meta-boxes.php @@ -903,12 +903,14 @@ function post_slug_meta_box( $post ) { */ function post_author_meta_box( $post ) { global $user_ID; + + $post_type_object = get_post_type_object( $post->post_type ); ?> 'authors', + 'capability' => array( $post_type_object->cap->edit_posts ), 'name' => 'post_author_override', 'selected' => empty( $post->ID ) ? $user_ID : $post->post_author, 'include_selected' => true, diff --git a/wp-content/themes/twentyfourteen/functions.php b/wp-content/themes/twentyfourteen/functions.php index f39296b1ba..892d6ac939 100644 --- a/wp-content/themes/twentyfourteen/functions.php +++ b/wp-content/themes/twentyfourteen/functions.php @@ -491,15 +491,24 @@ if ( ! function_exists( 'twentyfourteen_list_authors' ) ) : * @since Twenty Fourteen 1.0 */ function twentyfourteen_list_authors() { - $contributor_ids = get_users( - array( - 'fields' => 'ID', - 'orderby' => 'post_count', - 'order' => 'DESC', - 'who' => 'authors', - ) + $args = array( + 'fields' => 'ID', + 'orderby' => 'post_count', + 'order' => 'DESC', + 'capability' => array( 'edit_posts' ), ); + /** + * Filters query arguments for listing authors. + * + * @since 3.3 + * + * @param array $args Query arguments. + */ + $args = apply_filters( 'twentyfourteen_list_authors_query_args', $args ); + + $contributor_ids = get_users( $args ); + foreach ( $contributor_ids as $contributor_id ) : $post_count = count_user_posts( $contributor_id ); diff --git a/wp-includes/class-wp-user-query.php b/wp-includes/class-wp-user-query.php index f760c25288..936ffe89bf 100644 --- a/wp-includes/class-wp-user-query.php +++ b/wp-includes/class-wp-user-query.php @@ -93,6 +93,9 @@ class WP_User_Query { 'role' => '', 'role__in' => array(), 'role__not_in' => array(), + 'capability' => '', + 'capability__in' => array(), + 'capability__not_in' => array(), 'meta_key' => '', 'meta_value' => '', 'meta_compare' => '', @@ -133,6 +136,7 @@ class WP_User_Query { * querying for all users with using -1. * @since 4.7.0 Added 'nicename', 'nicename__in', 'nicename__not_in', 'login', 'login__in', * and 'login__not_in' parameters. + * @since 5.9.0 Added 'capability', 'capability__in', and 'capability__not_in' parameters. * * @global wpdb $wpdb WordPress database abstraction object. * @global int $blog_id @@ -148,6 +152,19 @@ class WP_User_Query { * roles. Default empty array. * @type string[] $role__not_in An array of role names to exclude. Users matching one or more of these * roles will not be included in results. Default empty array. + * @type string $capability An array or a comma-separated list of capability names that users must match + * to be included in results. Note that this is an inclusive list: users + * must match *each* capability. + * Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}. + * Default empty. + * @type string[] $capability__in An array of capability names. Matched users must have at least one of these + * capabilities. + * Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}. + * Default empty array. + * @type string[] $capability__not_in An array of capability names to exclude. Users matching one or more of these + * capabilities will not be included in results. + * Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}. + * Default empty array. * @type string $meta_key User meta key. Default empty. * @type string $meta_value User meta value. Default empty. * @type string $meta_compare Comparison operator to test the `$meta_value`. Accepts '=', '!=', @@ -320,6 +337,17 @@ class WP_User_Query { $this->meta_query->parse_query_vars( $qv ); if ( isset( $qv['who'] ) && 'authors' === $qv['who'] && $blog_id ) { + _deprecated_argument( + 'WP_User_Query', + '5.9.0', + sprintf( + /* translators: 1: who, 2: capability */ + __( '%1$s is deprecated. Use %2$s instead.' ), + 'who', + 'capability' + ) + ); + $who_query = array( 'key' => $wpdb->get_blog_prefix( $blog_id ) . 'user_level', 'value' => 0, @@ -343,6 +371,7 @@ class WP_User_Query { $this->meta_query->parse_query_vars( $this->meta_query->queries ); } + // Roles. $roles = array(); if ( isset( $qv['role'] ) ) { if ( is_array( $qv['role'] ) ) { @@ -362,6 +391,111 @@ class WP_User_Query { $role__not_in = (array) $qv['role__not_in']; } + // Capabilities. + $available_roles = array(); + + if ( ! empty( $qv['capability'] ) || ! empty( $qv['capability__in'] ) || ! empty( $qv['capability__not_in'] ) ) { + global $wp_roles; + + $wp_roles->for_site( $blog_id ); + $available_roles = $wp_roles->roles; + } + + $capabilities = array(); + if ( ! empty( $qv['capability'] ) ) { + if ( is_array( $qv['capability'] ) ) { + $capabilities = $qv['capability']; + } elseif ( is_string( $qv['capability'] ) ) { + $capabilities = array_map( 'trim', explode( ',', $qv['capability'] ) ); + } + } + + $capability__in = array(); + if ( ! empty( $qv['capability__in'] ) ) { + $capability__in = (array) $qv['capability__in']; + } + + $capability__not_in = array(); + if ( ! empty( $qv['capability__not_in'] ) ) { + $capability__not_in = (array) $qv['capability__not_in']; + } + + // Keep track of all capabilities and the roles they're added on. + $caps_with_roles = array(); + + foreach ( $available_roles as $role => $role_data ) { + $role_caps = array_keys( array_filter( $role_data['capabilities'] ) ); + + foreach ( $capabilities as $cap ) { + if ( in_array( $cap, $role_caps, true ) ) { + $caps_with_roles[ $cap ][] = $role; + break; + } + } + + foreach ( $capability__in as $cap ) { + if ( in_array( $cap, $role_caps, true ) ) { + $role__in[] = $role; + break; + } + } + + foreach ( $capability__not_in as $cap ) { + if ( in_array( $cap, $role_caps, true ) ) { + $role__not_in[] = $role; + break; + } + } + } + + $role__in = array_merge( $role__in, $capability__in ); + $role__not_in = array_merge( $role__not_in, $capability__not_in ); + + $roles = array_unique( $roles ); + $role__in = array_unique( $role__in ); + $role__not_in = array_unique( $role__not_in ); + + // Support querying by capabilities added directly to users. + if ( $blog_id && ! empty( $capabilities ) ) { + $capabilities_clauses = array( 'relation' => 'AND' ); + + foreach ( $capabilities as $cap ) { + $clause = array( 'relation' => 'OR' ); + + $clause[] = array( + 'key' => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities', + 'value' => '"' . $cap . '"', + 'compare' => 'LIKE', + ); + + if ( ! empty( $caps_with_roles[ $cap ] ) ) { + foreach ( $caps_with_roles[ $cap ] as $role ) { + $clause[] = array( + 'key' => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities', + 'value' => '"' . $role . '"', + 'compare' => 'LIKE', + ); + } + } + + $capabilities_clauses[] = $clause; + } + + $role_queries[] = $capabilities_clauses; + + if ( empty( $this->meta_query->queries ) ) { + $this->meta_query->queries[] = $capabilities_clauses; + } else { + // Append the cap query to the original queries and reparse the query. + $this->meta_query->queries = array( + 'relation' => 'AND', + array( $this->meta_query->queries, array( $capabilities_clauses ) ), + ); + } + + $this->meta_query->parse_query_vars( $this->meta_query->queries ); + } + if ( $blog_id && ( ! empty( $roles ) || ! empty( $role__in ) || ! empty( $role__not_in ) || is_multisite() ) ) { $role_queries = array(); diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index b3cdfc2e31..e3e5d935d7 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -198,6 +198,15 @@ class WP_REST_Users_Controller extends WP_REST_Controller { ); } + // Check if capabilities is specified in GET request and if user can list users. + if ( ! empty( $request['capabilities'] ) && ! current_user_can( 'list_users' ) ) { + return new WP_Error( + 'rest_user_cannot_view', + __( 'Sorry, you are not allowed to filter users by capability.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'rest_forbidden_context', @@ -254,13 +263,14 @@ class WP_REST_Users_Controller extends WP_REST_Controller { * present in $registered will be set. */ $parameter_mappings = array( - 'exclude' => 'exclude', - 'include' => 'include', - 'order' => 'order', - 'per_page' => 'number', - 'search' => 'search', - 'roles' => 'role__in', - 'slug' => 'nicename__in', + 'exclude' => 'exclude', + 'include' => 'include', + 'order' => 'order', + 'per_page' => 'number', + 'search' => 'search', + 'roles' => 'role__in', + 'capabilities' => 'capability__in', + 'slug' => 'nicename__in', ); $prepared_args = array(); @@ -1554,6 +1564,14 @@ class WP_REST_Users_Controller extends WP_REST_Controller { ), ); + $query_params['capabilities'] = array( + 'description' => __( 'Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ); + $query_params['who'] = array( 'description' => __( 'Limit result set to users who are considered authors.' ), 'type' => 'string', diff --git a/wp-includes/user.php b/wp-includes/user.php index 55bd546568..018c960c03 100644 --- a/wp-includes/user.php +++ b/wp-includes/user.php @@ -1320,13 +1320,32 @@ function wp_dropdown_users( $args = '' ) { 'role' => '', 'role__in' => array(), 'role__not_in' => array(), + 'capability' => '', + 'capability__in' => array(), + 'capability__not_in' => array(), ); $defaults['selected'] = is_author() ? get_query_var( 'author' ) : 0; $parsed_args = wp_parse_args( $args, $defaults ); - $query_args = wp_array_slice_assoc( $parsed_args, array( 'blog_id', 'include', 'exclude', 'orderby', 'order', 'who', 'role', 'role__in', 'role__not_in' ) ); + $query_args = wp_array_slice_assoc( + $parsed_args, + array( + 'blog_id', + 'include', + 'exclude', + 'orderby', + 'order', + 'who', + 'role', + 'role__in', + 'role__not_in', + 'capability', + 'capability__in', + 'capability__not_in', + ) + ); $fields = array( 'ID', 'user_login' ); diff --git a/wp-includes/version.php b/wp-includes/version.php index 9362fa7678..9ccc542e74 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '5.9-alpha-51942'; +$wp_version = '5.9-alpha-51943'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.