diff --git a/projects/plugins/jetpack/changelog/try-add-viewer-to-user-roles-response b/projects/plugins/jetpack/changelog/try-add-viewer-to-user-roles-response new file mode 100644 index 0000000000000..f5fe425b40d80 --- /dev/null +++ b/projects/plugins/jetpack/changelog/try-add-viewer-to-user-roles-response @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Users API: add 'viewer' to user role array. diff --git a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php index 86861753b9c27..29e74b4924ffc 100644 --- a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php +++ b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-list-users-endpoint.php @@ -42,8 +42,8 @@ ), 'response_format' => array( - 'found' => '(int) The total number of authors found that match the request (ignoring limits and offsets).', - 'authors' => '(array:author) Array of author objects.', + 'found' => '(int) The total number of authors found that match the request (ignoring limits and offsets).', + 'users' => '(array:user) Array of user objects.', ), 'example_response' => '{ @@ -164,87 +164,108 @@ public function callback( $path = '', $blog_id = 0 ) { $query['capability'] = $args['capability']; } - $user_query = new WP_User_Query( $query ); - remove_filter( 'user_search_columns', array( $this, 'api_user_override_search_columns' ) ); + $response = array(); + $users = array(); + $viewers = array(); $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM; $include_viewers = (bool) isset( $args['include_viewers'] ) && $args['include_viewers'] && $is_wpcom; - - $page = ( (int) ( $args['offset'] / $args['number'] ) ) + 1; - $viewers = $include_viewers ? get_private_blog_users( - $blog_id, - array( - 'page' => $page, - 'per_page' => $args['number'], - ) - ) : array(); - $viewers = array_map( array( $this, 'get_author' ), $viewers ); - - // When include_viewers is true, search by username or email. - if ( $include_viewers && ! empty( $args['search'] ) ) { - $viewers = array_filter( - $viewers, - function ( $viewer ) use ( $args ) { - // Convert to WP_User so expected fields are available. - $wp_viewer = new WP_User( $viewer->ID ); - // remove special database search characters from search term - $search_term = str_replace( '*', '', $args['search'] ); - return ( str_contains( $wp_viewer->user_login, $search_term ) || str_contains( $wp_viewer->user_email, $search_term ) || str_contains( $wp_viewer->display_name, $search_term ) ); + $is_multisite = is_multisite(); + $user_query = new WP_User_Query( $query ); + + // Get users. + foreach ( $user_query->get_results() as $u ) { + $the_user = $this->get_author( $u, true ); + if ( $the_user && ! is_wp_error( $the_user ) ) { + $userdata = get_userdata( $u ); + $the_user->roles = ! is_wp_error( $userdata ) ? array_values( $userdata->roles ) : array(); + if ( $is_multisite ) { + $the_user->is_super_admin = user_can( $the_user->ID, 'manage_network' ); } + $users[] = $the_user; + } + } + + // Get viewers. + if ( $include_viewers ) { + $page = ( (int) ( $args['offset'] / $args['number'] ) ) + 1; + $viewers = get_private_blog_users( + $blog_id, + array( + 'page' => $page, + 'per_page' => $args['number'], + ) ); + + // Returns author object. See `WPCOM_JSON_API_Endpoint::get_author` in `class.json-api-endpoints.php`. + $viewers = array_map( array( $this, 'get_author' ), $viewers ); + + // Search by username or email. + if ( ! empty( $args['search'] ) ) { + $viewers = array_filter( + $viewers, + function ( $viewer ) use ( $args ) { + // Convert to WP_User so expected fields are available. + $wp_viewer = new WP_User( $viewer->ID ); + // remove special database search characters from search term + $search_term = str_replace( '*', '', $args['search'] ); + return ( str_contains( $wp_viewer->user_login, $search_term ) || str_contains( $wp_viewer->user_email, $search_term ) || str_contains( $wp_viewer->display_name, $search_term ) ); + } + ); + } + + $viewer_ids = array(); + $unique_viewers = array(); + // Create a lookup array of viewer IDs. + foreach ( $viewers as $viewer ) { + $viewer_ids[ $viewer->ID ] = true; + $viewer->roles[] = 'viewer'; + } + + // Add viewer role to users who are also viewers. + foreach ( $users as $user ) { + if ( isset( $viewer_ids[ $user->ID ] ) && ! in_array( 'viewer', $user->roles, true ) ) { + $user->roles[] = 'viewer'; + // Mark this user so we don't add them again. + $viewer_ids[ $user->ID ] = false; + } + } + + // Add viewer role to viewers and remove duplicates. + foreach ( $viewers as $viewer ) { + if ( isset( $viewer_ids[ $viewer->ID ] ) && true === $viewer_ids[ $viewer->ID ] ) { + $viewer->roles[] = 'viewer'; + $unique_viewers[] = $viewer; + } + } + + // Reassign the viewers array to the unique viewers array. + $viewers = $unique_viewers; } - $return = array(); foreach ( array_keys( $this->response_format ) as $key ) { switch ( $key ) { case 'found': $user_count = (int) $user_query->get_total(); - - $viewer_count = 0; if ( $include_viewers ) { - if ( empty( $args['search'] ) ) { - $viewer_count = (int) get_count_private_blog_users( $blog_id ); - } else { - $viewer_count = count( $viewers ); - } + $viewer_count = count( $viewers ); + $response[ $key ] = $user_count + $viewer_count; + } else { + $response[ $key ] = $user_count; } - - $return[ $key ] = $user_count + $viewer_count; break; case 'users': - $users = array(); - $is_multisite = is_multisite(); - foreach ( $user_query->get_results() as $u ) { - $the_user = $this->get_author( $u, true ); - if ( $the_user && ! is_wp_error( $the_user ) ) { - $userdata = get_userdata( $u ); - $the_user->roles = ! is_wp_error( $userdata ) ? array_values( $userdata->roles ) : array(); - if ( $is_multisite ) { - $the_user->is_super_admin = user_can( $the_user->ID, 'manage_network' ); - } - $users[] = $the_user; - } - } - - $combined_users = array_merge( $users, $viewers ); - - // When viewers are included, we ignore the order & orderby parameters. if ( $include_viewers ) { - usort( - $combined_users, - function ( $a, $b ) { - return strcmp( strtolower( $a->name ), strtolower( $b->name ) ); - } - ); + $response[ $key ] = array_merge( $users, $viewers ); + } else { + $response[ $key ] = $users; } - - $return[ $key ] = $combined_users; break; } } - return $return; + return $response; } /** diff --git a/projects/plugins/jetpack/tests/php/json-api/test-class.wpcom-json-api-list-users-endpoint.php b/projects/plugins/jetpack/tests/php/json-api/test-class.wpcom-json-api-list-users-endpoint.php new file mode 100644 index 0000000000000..bc0958de1c5a9 --- /dev/null +++ b/projects/plugins/jetpack/tests/php/json-api/test-class.wpcom-json-api-list-users-endpoint.php @@ -0,0 +1,164 @@ +user->create( array( 'role' => 'administrator' ) ); + static::$user_editor_id = self::factory()->user->create( array( 'role' => 'editor' ) ); + wp_set_current_user( static::$user_admin_id ); + } + + /** + * Reset the environment to its original state after the test. + */ + public function tear_down() { + wp_delete_user( static::$user_admin_id ); + wp_delete_user( static::$user_editor_id ); + + parent::tear_down(); + } + + /** + * Returns the response of a successful GET request to `sites/%s/users`. + */ + public function make_get_request( $query_args = array() ) { + global $blog_id; + + $endpoint = new WPCOM_JSON_API_List_Users_Endpoint( + array( + 'description' => 'List the users of a site.', + 'group' => 'users', + 'stat' => 'users:list', + 'method' => 'GET', + 'path' => '/sites/%s/users', + 'path_labels' => array( + '$site' => '(int|string) Site ID or domain', + ), + 'query_parameters' => array( + 'number' => '(int=20) Limit the total number of authors returned.', + 'offset' => '(int=0) The first n authors to be skipped in the returned array.', + 'order' => array( + 'DESC' => 'Return authors in descending order.', + 'ASC' => 'Return authors in ascending order.', + ), + 'order_by' => array( + 'ID' => 'Order by ID (default).', + 'login' => 'Order by username.', + 'nicename' => 'Order by nicename.', + 'email' => 'Order by author email address.', + 'url' => 'Order by author URL.', + 'registered' => 'Order by registered date.', + 'display_name' => 'Order by display name.', + 'post_count' => 'Order by number of posts published.', + ), + 'authors_only' => '(bool) Set to true to fetch authors only', + 'include_viewers' => '(bool) Set to true to include viewers for Simple sites. When you pass in this parameter, order, order_by and search_columns are ignored. Currently, `search` is limited to the first page of results.', + 'type' => "(string) Specify the post type to query authors for. Only works when combined with the `authors_only` flag. Defaults to 'post'. Post types besides post and page need to be whitelisted using the rest_api_allowed_post_types filter.", + 'search' => '(string) Find matching users.', + 'search_columns' => "(array) Specify which columns to check for matching users. Can be any of 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', and 'display_name'. Only works when combined with `search` parameter.", + 'role' => '(string) Specify a specific user role to fetch.', + 'capability' => '(string) Specify a specific capability to fetch. You can specify multiple by comma separating them, in which case the user needs to match all capabilities provided.', + ), + 'response_format' => array( + 'found' => '(int) The total number of authors found that match the request (ignoring limits and offsets).', + 'authors' => '(array:author) Array of author objects.', + ), + 'example_response' => '{ + "found": 1, + "users": [ + { + "ID": 78972699, + "login": "apiexamples", + "email": "justin+apiexamples@a8c.com", + "name": "apiexamples", + "first_name": "", + "last_name": "", + "nice_name": "apiexamples", + "URL": "http://apiexamples.wordpress.com", + "avatar_URL": "https://1.gravatar.com/avatar/a2afb7b6c0e23e5d363d8612fb1bd5ad?s=96&d=identicon&r=G", + "profile_URL": "https://gravatar.com/apiexamples", + "site_ID": 82974409, + "roles": [ + "administrator" + ], + "is_super_admin": false + } + ] + }', + 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/82974409/users', + 'example_request_data' => array( + 'headers' => array( + 'authorization' => 'Bearer YOUR_API_TOKEN', + ), + ), + ) + ); + + $endpoint->api->query = $query_args; + + return $endpoint->callback( '/sites/%s/users', $blog_id ); + } + + /** + * Test GET `sites/%s/users` returns correct users. + */ + public function test_get_users_returns_correct_users() { + $response = $this->make_get_request(); + $users = $response['users']; + + // Find admin user. + $admin_user = array_filter( + $users, + function ( $user ) { + return static::$user_admin_id === $user->ID; + } + ); + $admin_user = reset( $admin_user ); + + // Find editor user. + $editor_user = array_filter( + $users, + function ( $user ) { + return static::$user_editor_id === $user->ID; + } + ); + $editor_user = reset( $editor_user ); + + // Assert user IDs and roles. + $this->assertNotNull( $admin_user, 'Admin user not found' ); + $this->assertEquals( static::$user_admin_id, $admin_user->ID, 'Admin user ID is not correct' ); + $this->assertContains( 'administrator', $admin_user->roles, 'Admin user roles are not correct' ); + + $this->assertNotNull( $editor_user, 'Editor user not found' ); + $this->assertEquals( static::$user_editor_id, $editor_user->ID, 'Editor user ID is not correct' ); + $this->assertContains( 'editor', $editor_user->roles, 'Editor user roles are not correct' ); + } +}