Skip to content

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

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

Draft
wants to merge 7 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 @@ -171,87 +171,107 @@ 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;
}

// 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 );
Copy link
Member Author

Choose a reason for hiding this comment

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

This won't be accurate due to legacy duplication. See: Automattic/wp-calypso#100674 (comment)

$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;
Copy link
Member Author

@ramonjd ramonjd Feb 17, 2025

Choose a reason for hiding this comment

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

Something about these changes also screws with the /people/viewers/$wordpress_site/$user_id Calypso page. Check.

Copy link
Member Author

@ramonjd ramonjd Feb 27, 2025

Choose a reason for hiding this comment

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

It's this check: https://github.com/Automattic/wp-calypso/blob/trunk/client/my-sites/people/team-members/index.tsx#L114

It assumes that the roles array is empty for viewers.

It should be something like const type = user.roles?.length && user.roles?.includes( 'viewer' ) ? 'viewer' : 'email';

Copy link
Member Author

@ramonjd ramonjd Feb 27, 2025

Choose a reason for hiding this comment

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

Also watch out for double badges here:

https://github.com/Automattic/wp-calypso/blob/trunk/client/my-sites/people/people-profile/index.jsx#L320-L321

Screenshot 2025-02-27 at 5 46 05 pm

If this PR gets up, we can remove { type === 'viewer' && renderViewerRole() } because getRole() looks in the role array.

}

/**
Expand Down
Loading