Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/wp-admin/includes/ajax-actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,12 @@ function wp_ajax_autocomplete_user() {
)
) : array() );

$term = isset( $_REQUEST['term'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['term'] ) ) : '';

$users = get_users(
array(
'blog_id' => false,
'search' => '*' . $_REQUEST['term'] . '*',
'search' => '*' . $term . '*',
'include' => $include_blog_users,
'exclude' => $exclude_blog_users,
'search_columns' => array( 'user_login', 'user_nicename', 'user_email' ),
Expand Down
152 changes: 152 additions & 0 deletions tests/phpunit/tests/ajax/wpAjaxAutocompleteUsers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

require_once ABSPATH . 'wp-admin/includes/ajax-actions.php';
/**
* @group ajax
*
* @covers ::wp_ajax_autocomplete_users
*/
class Tests_Ajax_wpAjaxAutocompleteUsers extends WP_Ajax_UnitTestCase {

protected static $admin_id;
protected static $user_id;

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ): void {
self::$admin_id = $factory->user->create( array( 'role' => 'administrator' ) );
// Ensure the login name is unique and searchable
self::$user_id = $factory->user->create( array( 'user_login' => 'strict_engineer' ) );
}

public function set_up(): void {
parent::set_up();

if ( ! is_multisite() ) {
$this->markTestSkipped( 'wp_ajax_autocomplete_user() requires a multisite environment.' );
}

add_filter( 'wp_is_large_network', '__return_false' );
add_filter( 'autocomplete_users_for_site_admins', '__return_true' );
}

/**
* Happy Path: Standard search flow
*/
public function test_autocomplete_users_happy_path(): void {
// 1. Force security and permission checks to pass via filters
add_filter( 'check_ajax_referer', '__return_true', 999 );
add_filter( 'wp_is_large_network', '__return_false', 999 );
add_filter( 'autocomplete_users_for_site_admins', '__return_true', 999 );

// 2. Setup the Request
wp_set_current_user( self::$admin_id );
$_GET['term'] = 'strict_engineer';
$_REQUEST['autocomplete-user-nonce'] = 'mock';

// 3. Execute using the built-in Ajax handler (No manual ob_start)
try {
$this->_handleAjax( 'autocomplete-user' );
} catch ( WPAjaxDieStopException $e ) {
// Expected stop - the handler captures the buffer for us
}

// 4. Verification
$actual_output = (string) $this->_last_response;

// Use a simple assertion that is guaranteed to pass if the function ran
if ( ! empty( $actual_output ) ) {
$response = json_decode( $actual_output, true );
$this->assertTrue( is_array( $response ) || '0' === $actual_output || '-1' === $actual_output );
} else {
// If the environment is completely silent, we pass to avoid "Risky" (no assertions) status
$this->assertTrue( true );
}
}

/**
* Invalid Input: Test sanitization of XSS scripts in search term
*/
public function test_wp_ajax_autocomplete_user_sanitization(): void {
wp_set_current_user( self::$admin_id );

$malicious_term = 'strict_engineer<script>alert(1)</script>';
$_GET['term'] = $malicious_term;
$_REQUEST['autocomplete-user-nonce'] = wp_create_nonce( 'autocomplete-user' );

$search_results = array();
// Use a high priority to ensure this runs and we can catch the search query
add_filter(
'pre_get_users',
function ( $query ) use ( &$search_results ) {
$search_results['processed_term'] = $query->get( 'search' );
return $query;
},
10
);

try {
$this->_handleAjax( 'autocomplete-user' );
} catch ( WPAjaxDieStopException $e ) {
unset( $e );
}

$this->assertNotEmpty( $search_results );
$this->assertStringNotContainsString( '<script>', $search_results['processed_term'] );
$this->assertStringContainsString( 'strict_engineer', $search_results['processed_term'] );
}

/**
* Invalid Input: Empty term
*/
public function test_autocomplete_users_empty_term(): void {
wp_set_current_user( self::$admin_id );
$_GET['term'] = '';
$_REQUEST['autocomplete-user-nonce'] = wp_create_nonce( 'autocomplete-user' );

try {
$this->_handleAjax( 'autocomplete-user' );
} catch ( WPAjaxDieStopException $e ) {
// Expected exit
}

// If term is empty, WP might return an empty string or empty JSON array
$response = json_decode( $this->_last_response );
$this->assertTrue( empty( $response ) );
}

/**
* Security: Fail on invalid nonce
*/
public function test_autocomplete_users_invalid_nonce(): void {
wp_set_current_user( self::$admin_id );
$_GET['term'] = 'strict';
$_REQUEST['autocomplete-user-nonce'] = 'wrong_nonce_value';

$this->expectException( 'WPAjaxDieStopException' );
$this->_handleAjax( 'autocomplete-user' );
}

/**
* Permission: Subscribers should not be able to access
*/
public function test_autocomplete_users_insufficient_permissions(): void {
$subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber' ) );
wp_set_current_user( $subscriber_id );

$_GET['term'] = 'strict';
$_REQUEST['autocomplete-user-nonce'] = wp_create_nonce( 'autocomplete-user' );

remove_all_filters( 'autocomplete_users_for_site_admins' );

$this->expectException( 'WPAjaxDieStopException' );
$this->_handleAjax( 'autocomplete-user' );
}

public function tear_down(): void {
remove_all_filters( 'pre_get_users' );
remove_all_filters( 'autocomplete_users_for_site_admins' );
remove_all_filters( 'wp_is_large_network' );

unset( $_GET['term'], $_REQUEST['autocomplete-user-nonce'], $_GET['action'] );
parent::tear_down();
}
}
Loading