Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Users API endpoint: add 'viewer' role to user roles array and dedupe return value #41707

Draft
wants to merge 5 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: other

Users API: add 'viewer' to user role array.
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '{
Expand Down Expand Up @@ -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 ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've refactored all this to test out grabbing, deduping and merging the user and viewer lists before we return arbitrary count values that are not accurate.

This would remove the need for frontend hacks like:

$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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php
/**
* Jetpack WPCOM JSON API `sites/%s/users` endpoint unit tests.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these basic tests because they weren't there.

Testing the viewer role might have to be done on WPCOM as it's gated behind the IS_WPCOM constant and calls internal methods, e.g., get_private_blog_users

* Run this test with command: jetpack docker phpunit -- --filter=WP_Test_WPCOM_JSON_API_List_Users_Endpoint
*
* @package automattic/jetpack
*/

require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';

/**
* Jetpack `sites/%s/users` endpoint unit tests.
*/
class WP_Test_WPCOM_JSON_API_List_Users_Endpoint extends WP_UnitTestCase {
/**
* Mock user ID with administrator permissions.
*
* @var int
*/
private static $user_admin_id = 0;

/**
* Mock user ID with editor permissions.
*
* @var int
*/
private static $user_editor_id = 0;

/**
* Prepare the environment for the test.
*/
public function set_up() {
parent::set_up();
static::$user_admin_id = self::factory()->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 <code>rest_api_allowed_post_types</code> 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": "[email protected]",
"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' );
}
}
Loading