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
9 changes: 9 additions & 0 deletions src/wp-includes/class-wp-user.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ public function init( $data, $site_id = 0 ) {
*
* @since 3.3.0
* @since 4.4.0 Added 'ID' as an alias of 'id' for the `$field` parameter.
* @since 7.1.0 Non-existent users looked up by ID are now cached to prevent duplicate queries.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
Expand Down Expand Up @@ -251,13 +252,21 @@ public static function get_data_by( $field, $value ) {
}
}

// For ID lookups, check if this user has been cached as non-existent.
if ( 'id' === $field && wp_cache_get( 'notuser_' . $user_id, 'users' ) ) {
return false;
}

$user = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM $wpdb->users WHERE $db_field = %s LIMIT 1",
$value
)
);
if ( ! $user ) {
if ( 'id' === $field ) {
wp_cache_set( 'notuser_' . $user_id, true, 'users', DAY_IN_SECONDS );
}
return false;
}

Expand Down
7 changes: 7 additions & 0 deletions src/wp-includes/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -2013,18 +2013,25 @@ function update_user_caches( $user ) {
* @since 3.0.0
* @since 4.4.0 'clean_user_cache' action was added.
* @since 6.2.0 User metadata caches are now cleared.
* @since 7.1.0 The non-existent user cache is cleared before looking up user data.
*
* @param WP_User|int $user User object or ID to be cleaned from the cache
*/
function clean_user_cache( $user ) {
if ( is_numeric( $user ) ) {
/*
* Clear the non-existent user cache before constructing WP_User, so that
* get_data_by() can reach the database and return the real user data.
*/
wp_cache_delete( 'notuser_' . (int) $user, 'users' );
$user = new WP_User( $user );
}

if ( ! $user->exists() ) {
return;
}

wp_cache_delete( 'notuser_' . $user->ID, 'users' );
wp_cache_delete( $user->ID, 'users' );
wp_cache_delete( $user->user_login, 'userlogins' );
wp_cache_delete( $user->user_nicename, 'userslugs' );
Expand Down
115 changes: 115 additions & 0 deletions tests/phpunit/tests/user/getDataBy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/**
* Tests for WP_User::get_data_by() caching behavior.
*
* @group user
* @group cache
*
* @coversDefaultClass WP_User
*/
class Tests_User_GetDataBy extends WP_UnitTestCase {

/**
* @ticket 46388
* @covers WP_User::get_data_by
*/
public function test_nonexistent_user_by_id_does_not_trigger_multiple_queries() {
global $wpdb;

$nonexistent_id = PHP_INT_MAX;

wp_cache_delete( 'notuser_' . $nonexistent_id, 'users' );
wp_cache_delete( $nonexistent_id, 'users' );

$before = $wpdb->num_queries;
get_userdata( $nonexistent_id );
$after_first_call = $wpdb->num_queries;

get_userdata( $nonexistent_id );
$after_second_call = $wpdb->num_queries;

$this->assertSame( 1, $after_first_call - $before, 'First call for non-existent user should trigger one DB query.' );
$this->assertSame( 0, $after_second_call - $after_first_call, 'Second call for non-existent user should not trigger any DB queries.' );
}

/**
* @ticket 46388
* @covers WP_User::get_data_by
*/
public function test_nonexistent_user_by_id_is_added_to_notuser_cache() {
$nonexistent_id = PHP_INT_MAX - 1;

wp_cache_delete( 'notuser_' . $nonexistent_id, 'users' );
wp_cache_delete( $nonexistent_id, 'users' );

get_userdata( $nonexistent_id );

$this->assertNotFalse(
wp_cache_get( 'notuser_' . $nonexistent_id, 'users' ),
'Non-existent user ID should be stored in the notuser cache after a DB miss.'
);
}

/**
* @ticket 46388
* @covers WP_User::get_data_by
*/
public function test_existing_user_by_id_is_not_added_to_notuser_cache() {
$user_id = self::factory()->user->create();

get_userdata( $user_id );

$this->assertFalse(
wp_cache_get( 'notuser_' . $user_id, 'users' ),
'Existing user ID should not be stored in the notuser cache.'
);
}

/**
* Verifies that clean_user_cache() called with a plain integer (as in wp_insert_user())
* clears the notuser cache before constructing WP_User internally,
* preventing a self-referential cache miss.
*
* @ticket 46388
* @covers ::clean_user_cache
*/
public function test_clean_user_cache_with_integer_clears_notuser_cache() {
$user_id = self::factory()->user->create();

// Manually poison the notuser cache for this user.
wp_cache_delete( $user_id, 'users' );
wp_cache_set( 'notuser_' . $user_id, true, 'users' );

// wp_insert_user() calls clean_user_cache() with a plain integer.
clean_user_cache( $user_id );

$this->assertFalse(
wp_cache_get( 'notuser_' . $user_id, 'users' ),
'clean_user_cache() with an integer must clear notuser_ before constructing WP_User.'
);
}

/**
* Verifies that clean_user_cache() called with a WP_User object (as in wp_update_user())
* also clears the notuser cache.
*
* @ticket 46388
* @covers ::clean_user_cache
*/
public function test_clean_user_cache_with_wp_user_object_clears_notuser_cache() {
$user_id = self::factory()->user->create();
$user_obj = get_userdata( $user_id );

// Manually poison the notuser cache.
wp_cache_set( 'notuser_' . $user_id, true, 'users' );

// wp_update_user() calls clean_user_cache() with a WP_User object.
clean_user_cache( $user_obj );

$this->assertFalse(
wp_cache_get( 'notuser_' . $user_id, 'users' ),
'clean_user_cache() with a WP_User object must clear the notuser_ cache entry.'
);
}
}
Loading