Skip to content
Merged
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
30 changes: 23 additions & 7 deletions includes/class-map-rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,23 +292,39 @@ public static function revoke_invite( \WP_REST_Request $request ): \WP_REST_Resp
* @return \WP_REST_Response
*/
public static function get_suggested_authors( \WP_REST_Request $request ): \WP_REST_Response {
$post_id = (int) $request->get_param( 'post_id' );
$search = (string) $request->get_param( 'search' );
$post_id = (int) $request->get_param( 'post_id' );
$search = (string) $request->get_param( 'search' );

// Require a minimum search length to prevent user enumeration.
// On large networks, require a longer search term to limit scope.
$is_large_network = function_exists( 'wp_is_large_network' ) && \wp_is_large_network( 'users' );
$min_length = $is_large_network ? 5 : 2;
if ( mb_strlen( $search ) < $min_length ) {
return new \WP_REST_Response( array(), 200 );
}

$post = get_post( $post_id );
$existing_ids = Co_Authors::get_co_authors( $post_id );
$existing_ids[] = (int) $post->post_author;

// On large networks, restrict to indexed columns only for performance.
// Allow email search only for users who can already view user details.
$search_columns = $is_large_network
? array( 'user_login', 'user_nicename' )
: array( 'user_login', 'user_nicename', 'display_name' );

if ( current_user_can( 'list_users' ) ) {
$search_columns[] = 'user_email';
}

$args = array(
'capability' => array( 'edit_posts' ),
'number' => 20,
'exclude' => $existing_ids,
'search_columns' => array( 'user_login', 'user_nicename', 'user_email', 'display_name' ),
'search' => '*' . $search . '*',
'search_columns' => $search_columns,
);

if ( '' !== $search ) {
$args['search'] = '*' . $search . '*';
}

$users = get_users( $args );
$result = array();
foreach ( $users as $user ) {
Expand Down
79 changes: 79 additions & 0 deletions tests/Test_Rest_API.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,85 @@ public function test_invite_management_forbidden_for_co_author(): void {
$this->assertSame( 403, $response->get_status() );
}

// -------------------------------------------------------------------------
// Suggested authors
// -------------------------------------------------------------------------

public function test_suggested_authors_returns_empty_for_short_search(): void {
wp_set_current_user( $this->author_id );

$request = new WP_REST_Request( 'GET', '/multi-author-posts/v1/posts/' . $this->post_id . '/suggested-authors' );
$request->set_param( 'search', 'a' );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status() );
$this->assertSame( array(), $response->get_data() );
}

public function test_suggested_authors_returns_empty_for_empty_search(): void {
wp_set_current_user( $this->author_id );

$request = new WP_REST_Request( 'GET', '/multi-author-posts/v1/posts/' . $this->post_id . '/suggested-authors' );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status() );
$this->assertSame( array(), $response->get_data() );
}

public function test_suggested_authors_does_not_search_by_email_for_authors(): void {
$target = self::factory()->user->create(
array(
'role' => 'author',
'user_email' => 'unique-test-email@example.com',
'display_name' => 'Test Target',
)
);
wp_set_current_user( $this->author_id );

$request = new WP_REST_Request( 'GET', '/multi-author-posts/v1/posts/' . $this->post_id . '/suggested-authors' );
$request->set_param( 'search', 'unique-test-email' );
$response = rest_get_server()->dispatch( $request );

$ids = array_column( $response->get_data(), 'id' );
$this->assertNotContains( $target, $ids );
}

public function test_suggested_authors_searches_by_email_for_admins(): void {
$admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
$target = self::factory()->user->create(
array(
'role' => 'author',
'user_email' => 'admin-findable@example.com',
'display_name' => 'Hidden Name',
)
);
wp_set_current_user( $admin );

$request = new WP_REST_Request( 'GET', '/multi-author-posts/v1/posts/' . $this->post_id . '/suggested-authors' );
$request->set_param( 'search', 'admin-findable' );
$response = rest_get_server()->dispatch( $request );

$ids = array_column( $response->get_data(), 'id' );
$this->assertContains( $target, $ids );
}

public function test_suggested_authors_returns_results_for_valid_search(): void {
$target = self::factory()->user->create(
array(
'role' => 'author',
'display_name' => 'Searchable Author',
)
);
wp_set_current_user( $this->author_id );

$request = new WP_REST_Request( 'GET', '/multi-author-posts/v1/posts/' . $this->post_id . '/suggested-authors' );
$request->set_param( 'search', 'Searchable' );
$response = rest_get_server()->dispatch( $request );

$ids = array_column( $response->get_data(), 'id' );
$this->assertContains( $target, $ids );
}

public function test_editor_can_manage_co_authors_via_edit_others_posts(): void {
// An editor (who has edit_others_posts) can manage co-authors even
// though they are not the post author.
Expand Down