From e0ced67002d2d491110e86909a912af7a9d03f8f Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Wed, 28 Jun 2023 18:34:21 -0600 Subject: [PATCH 1/5] Check cases only for new users, not updates. New tests. --- src/wp-includes/user.php | 16 ++++++++++++-- tests/phpunit/tests/user.php | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 8e230708e4ee0..5e9195ed9d488 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -2124,8 +2124,18 @@ function wp_insert_user( $userdata ) { return new WP_Error( 'user_login_too_long', __( 'Username may not be longer than 60 characters.' ) ); } - if ( ! $update && username_exists( $user_login ) ) { - return new WP_Error( 'existing_user_login', __( 'Sorry, that username already exists!' ) ); + // Check additional restrictions for new users. + if ( ! $update ) { + + // Username must be unique. + if ( username_exists( $user_login ) ) { + return new WP_Error( 'existing_user_login', __( 'Sorry, that username already exists!' ) ); + } + + // Username must not match an existing user email. + if ( email_exists( $user_login ) ) { + return new WP_Error( 'existing_user_email_as_login', __( 'Sorry, that username is not available.' ) ); + } } /** @@ -3354,6 +3364,8 @@ function register_new_user( $user_login, $user_email ) { $sanitized_user_login = ''; } elseif ( username_exists( $sanitized_user_login ) ) { $errors->add( 'username_exists', __( 'Error: This username is already registered. Please choose another one.' ) ); + } elseif ( email_exists( $sanitized_user_login ) ) { + $errors->add( 'username_exists_as_email', __( 'Error: This username is not available. Please choose another one.' ) ); } else { /** This filter is documented in wp-includes/user.php */ $illegal_user_logins = (array) apply_filters( 'illegal_user_logins', array() ); diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php index 7157a00cfa9f5..e32c971a15894 100644 --- a/tests/phpunit/tests/user.php +++ b/tests/phpunit/tests/user.php @@ -915,6 +915,49 @@ public function test_wp_insert_user_should_reject_user_nicename_over_50_characte $this->assertSame( 'user_nicename_too_long', $u->get_error_code() ); } + + /** + * @ticket 57394 + */ + public function test_wp_insert_user_should_reject_user_login_that_matches_existing_user_email() { + // When creating a user, the user_login should not match an existing user's email. + $existing_email = get_option( 'admin_email' ); + $user_id = wp_insert_user( + array( + 'user_login' => $existing_email, + 'user_email' => 'whatever@example.com', + 'user_pass' => 'whatever', + 'user_nicename' => 'whatever', + ) + ); + $this->assertWPError( $user_id ); + $this->assertSame( 'existing_user_email_as_login', $user_id->get_error_code() ); + + // When updating a user, the user_login can match an existing user's email. + $new_email = 'new.email@example.com'; + + $user_id = wp_insert_user( + array( + 'user_login' => 'testuser', + 'user_email' => $new_email, + 'user_pass' => 'whatever', + 'user_nicename' => 'whatever', + ) + ); + $this->assertNotWPError( $user_id ); + + $user_id = wp_update_user( + array( + 'ID' => $user_id, + 'user_login' => $new_email, + ) + ); + // No error should be returned. + $this->assertNotWPError( $user_id ); + // The user login should have been updated. + $this->assertSame( $new_email, get_userdata( $user_id )->user_login ); + } + /** * @ticket 33793 */ From 4876397b6dd5d63ddb443f1372714ca5c5f1e2c5 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Wed, 28 Jun 2023 21:06:00 -0600 Subject: [PATCH 2/5] phpcbf --- tests/phpunit/tests/user.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php index e32c971a15894..9727439d02455 100644 --- a/tests/phpunit/tests/user.php +++ b/tests/phpunit/tests/user.php @@ -916,7 +916,7 @@ public function test_wp_insert_user_should_reject_user_nicename_over_50_characte } - /** + /** * @ticket 57394 */ public function test_wp_insert_user_should_reject_user_login_that_matches_existing_user_email() { @@ -948,8 +948,8 @@ public function test_wp_insert_user_should_reject_user_login_that_matches_existi $user_id = wp_update_user( array( - 'ID' => $user_id, - 'user_login' => $new_email, + 'ID' => $user_id, + 'user_login' => $new_email, ) ); // No error should be returned. From c084aaeb6994168918687b586749efbf3d3e5c4a Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Wed, 28 Jun 2023 21:27:55 -0600 Subject: [PATCH 3/5] Fix up tests; now pass (and fail before current changes) --- tests/phpunit/tests/user.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php index 9727439d02455..044c7ccbd6a0c 100644 --- a/tests/phpunit/tests/user.php +++ b/tests/phpunit/tests/user.php @@ -938,7 +938,7 @@ public function test_wp_insert_user_should_reject_user_login_that_matches_existi $user_id = wp_insert_user( array( - 'user_login' => 'testuser', + 'user_login' => $new_email, 'user_email' => $new_email, 'user_pass' => 'whatever', 'user_nicename' => 'whatever', @@ -948,14 +948,16 @@ public function test_wp_insert_user_should_reject_user_login_that_matches_existi $user_id = wp_update_user( array( - 'ID' => $user_id, - 'user_login' => $new_email, + 'ID' => $user_id, + 'user_login' => $new_email, + 'user_email' => $new_email, + 'user_nicename' => 'new-nicename', ) ); // No error should be returned. $this->assertNotWPError( $user_id ); - // The user login should have been updated. - $this->assertSame( $new_email, get_userdata( $user_id )->user_login ); + // The user_nicename should have been updated. + $this->assertSame( 'new-nicename', get_userdata( $user_id )->user_nicename ); } /** From 6eed32dd98126741664f15244344b2580662309c Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 6 Oct 2026 16:47:56 +0200 Subject: [PATCH 4/5] Users: Only check new or changed values for username/email collisions. The check reverted in r55584 ran on every save, so updating a user whose username matches their own email, or who already collided with another user, failed. Usernames can only be set on insert, and emails are now compared against existing usernames only when they are new or changing, excluding the user being saved. Existing collisions are left alone, so no previously valid update starts failing. The same email-vs-username check is mirrored in register_new_user(), edit_user(), the REST users update endpoint and the profile email change confirmation, so users see a form error instead of a generic failure. Imports skip the checks, matching the existing email uniqueness check. Claude-Session: https://claude.ai/code/session_01XXGeBhJbUuzyPyZhFKKM5T --- src/wp-admin/includes/user.php | 11 +++++ .../class-wp-rest-users-controller.php | 13 ++++++ src/wp-includes/user.php | 40 ++++++++++++------- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index c6c08c4eaf76c..e0aea01f6d465 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -201,6 +201,10 @@ function edit_user( $user_id = 0 ) { $errors->add( 'user_login', __( 'Error: This username is already registered. Please choose another one.' ) ); } + if ( ! $update && email_exists( $user->user_login ) ) { + $errors->add( 'user_login', __( 'Error: This username is not available. Please choose another one.' ) ); + } + /** This filter is documented in wp-includes/user.php */ $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); @@ -218,6 +222,13 @@ function edit_user( $user_id = 0 ) { if ( $owner_id && ( ! $update || ( $owner_id !== $user->ID ) ) ) { $errors->add( 'email_exists', __( 'Error: This email is already registered. Please choose another one.' ), array( 'form-field' => 'email' ) ); } + + // Only check a new or changed email address against existing usernames. + $is_new_email = ! $update || 0 !== strcasecmp( $user->user_email, $userdata->user_email ); + $login_owner_id = $is_new_email ? username_exists( $user->user_email ) : false; + if ( $login_owner_id && ( ! $update || ( (int) $login_owner_id !== $user->ID ) ) ) { + $errors->add( 'email_exists_as_username', __( 'Error: This email address is not available. Please choose another one.' ), array( 'form-field' => 'email' ) ); + } } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index 9b25cf7974cbc..3d46b890ac5be 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -782,6 +782,19 @@ public function update_item( $request ) { ); } + // A new email address must not match another user's username. + if ( is_string( $request['email'] ) && 0 !== strcasecmp( $request['email'], $user->user_email ) ) { + $login_owner_id = username_exists( $request['email'] ); + + if ( $login_owner_id && (int) $login_owner_id !== $id ) { + return new WP_Error( + 'rest_user_invalid_email', + __( 'Invalid email address.' ), + array( 'status' => 400 ) + ); + } + } + if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) { return new WP_Error( 'rest_user_invalid_argument', diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 3752038dfe492..9d46f98a91635 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -2351,16 +2351,18 @@ function wp_insert_user( $userdata ) { return new WP_Error( 'user_login_too_long', __( 'Username may not be longer than 60 characters.' ) ); } - // Check additional restrictions for new users. + /* + * The username can only be set when creating a user, so these checks never + * run on update. This keeps existing users editable even if their username + * already matches another user's email address. + */ if ( ! $update ) { - - // Username must be unique. if ( username_exists( $user_login ) ) { return new WP_Error( 'existing_user_login', __( 'Sorry, that username already exists!' ) ); } - // Username must not match an existing user email. - if ( email_exists( $user_login ) ) { + // Username must not match another user's email address. + if ( ! defined( 'WP_IMPORTING' ) && email_exists( $user_login ) ) { return new WP_Error( 'existing_user_email_as_login', __( 'Sorry, that username is not available.' ) ); } } @@ -2432,15 +2434,21 @@ function wp_insert_user( $userdata ) { $user_email = apply_filters( 'pre_user_email', $raw_user_email ); /* - * If there is no update, just check for `email_exists`. If there is an update, - * check if current email and new email are the same, and check `email_exists` - * accordingly. + * Only validate the email address when creating a user or when the email + * address is changing, so that existing users can always be updated. */ - if ( ( ! $update || ( ! empty( $old_user_data ) && 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) ) - && ! defined( 'WP_IMPORTING' ) - && email_exists( $user_email ) - ) { - return new WP_Error( 'existing_user_email', __( 'Sorry, that email address is already used!' ) ); + $is_new_email = ! $update || ( ! empty( $old_user_data ) && 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ); + + if ( $is_new_email && ! defined( 'WP_IMPORTING' ) ) { + if ( email_exists( $user_email ) ) { + return new WP_Error( 'existing_user_email', __( 'Sorry, that email address is already used!' ) ); + } + + // Email address must not match another user's username. + $login_owner_id = username_exists( $user_email ); + if ( $login_owner_id && ( ! $update || (int) $login_owner_id !== $user_id ) ) { + return new WP_Error( 'existing_user_login_as_email', __( 'Sorry, that email address is not available.' ) ); + } } $raw_user_url = empty( $userdata['user_url'] ) ? '' : $userdata['user_url']; @@ -3631,6 +3639,8 @@ function register_new_user( $user_login, $user_email ) { esc_url( wp_login_url() ) ) ); + } elseif ( username_exists( $user_email ) ) { + $errors->add( 'email_exists_as_username', __( 'Error: This email address is not available. Please choose another one.' ) ); } /** @@ -3936,7 +3946,9 @@ function send_confirmation_on_profile_email( $user_id = 0 ) { return; } - if ( email_exists( $_POST['email'] ) ) { + $login_owner_id = username_exists( $_POST['email'] ); + + if ( email_exists( $_POST['email'] ) || ( $login_owner_id && (int) $login_owner_id !== $current_user->ID ) ) { $errors->add( 'user_email', __( 'Error: The email address is already used.' ), From f7ae877c73a00396cac80bb972c1dca2887eb3f5 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 6 Oct 2026 16:59:54 +0200 Subject: [PATCH 5/5] Tests: Cover username/email collision checks on insert and update. Data providers spell out which saves must be blocked and which must keep working, including the cases behind the r55584 revert: a user whose username is their own email, and users whose existing data already collides with another account. Collisions are seeded directly in the database to mirror sites that predate the check. Replaces the earlier single test, which covered only one update case. Claude-Session: https://claude.ai/code/session_01XXGeBhJbUuzyPyZhFKKM5T --- .../tests/rest-api/rest-users-controller.php | 42 ++ tests/phpunit/tests/user.php | 415 ++++++++++++++++-- 2 files changed, 430 insertions(+), 27 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-users-controller.php b/tests/phpunit/tests/rest-api/rest-users-controller.php index aa68de786740d..05efd35b10b68 100644 --- a/tests/phpunit/tests/rest-api/rest-users-controller.php +++ b/tests/phpunit/tests/rest-api/rest-users-controller.php @@ -1820,6 +1820,48 @@ public function test_update_item_existing_email_case_not_own() { $this->assertSame( 'rest_user_invalid_email', $data['code'] ); } + /** + * @ticket 57394 + */ + public function test_update_item_email_matching_another_user_login() { + self::factory()->user->create( + array( + 'user_login' => 'login-57394@example.com', + 'user_email' => 'other-57394@example.com', + ) + ); + + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', self::$editor ) ); + $request->set_param( 'email', 'login-57394@example.com' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_user_invalid_email', $response, 400 ); + } + + /** + * @ticket 57394 + */ + public function test_update_item_email_matching_own_login() { + $user_id = self::factory()->user->create( + array( + 'role' => 'editor', + 'user_login' => 'self-57394@example.com', + 'user_email' => 'self-other-57394@example.com', + ) + ); + + wp_set_current_user( $user_id ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) ); + $request->set_param( 'email', 'self-57394@example.com' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'self-57394@example.com', $response->get_data()['email'] ); + } + public function test_update_item_invalid_locale() { $user1 = self::factory()->user->create( array( diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php index abf8856d39eed..d9cfa86d1d242 100644 --- a/tests/phpunit/tests/user.php +++ b/tests/phpunit/tests/user.php @@ -1372,47 +1372,408 @@ public function test_wp_insert_user_should_reject_user_nicename_over_50_characte /** + * Creates two users whose login and email are set directly in the database. + * + * Writing straight to the table bypasses wp_insert_user() validation, which + * makes it possible to reproduce collisions that already exist on live sites. + * + * @param array $user_a Login and email for user A. + * @param array $user_b Login and email for user B. + * @return int[] IDs of user A and user B. + */ + private function create_users_with_logins_and_emails( $user_a, $user_b ) { + global $wpdb; + + $ids = array(); + foreach ( array( $user_a, $user_b ) as $user ) { + $id = self::factory()->user->create(); + $wpdb->update( + $wpdb->users, + array( + 'user_login' => $user['login'], + 'user_email' => $user['email'], + ), + array( 'ID' => $id ) + ); + clean_user_cache( $id ); + $ids[] = $id; + } + + return $ids; + } + + /** + * Tests that wp_insert_user() rejects a new user whose login or email + * collides with another user's email or login. + * * @ticket 57394 + * + * @covers ::wp_insert_user + * + * @dataProvider data_wp_insert_user_login_email_collisions + * + * @param array $userdata User data for the new user. + * @param string|null $expected_error Expected error code, or null for success. */ - public function test_wp_insert_user_should_reject_user_login_that_matches_existing_user_email() { - // When creating a user, the user_login should not match an existing user's email. - $existing_email = get_option( 'admin_email' ); - $user_id = wp_insert_user( + public function test_wp_insert_user_login_email_collisions( $userdata, $expected_error ) { + $this->create_users_with_logins_and_emails( + array( + 'login' => 'existing', + 'email' => 'existing@example.com', + ), array( - 'user_login' => $existing_email, - 'user_email' => 'whatever@example.com', - 'user_pass' => 'whatever', - 'user_nicename' => 'whatever', + 'login' => 'existing-login@example.com', + 'email' => 'other@example.com', ) ); - $this->assertWPError( $user_id ); - $this->assertSame( 'existing_user_email_as_login', $user_id->get_error_code() ); - // When updating a user, the user_login can match an existing user's email. - $new_email = 'new.email@example.com'; + $userdata['user_pass'] = 'password'; + $result = wp_insert_user( $userdata ); + + if ( null === $expected_error ) { + $this->assertNotWPError( $result ); + } else { + $this->assertWPError( $result, 'A WP_Error object should have been returned.' ); + $this->assertSame( $expected_error, $result->get_error_code(), 'An unexpected error code was returned.' ); + } + } + + /** + * Data provider for test_wp_insert_user_login_email_collisions(). + * + * @return array[] + */ + public function data_wp_insert_user_login_email_collisions() { + return array( + 'unique login and email' => array( + 'userdata' => array( + 'user_login' => 'new', + 'user_email' => 'new@example.com', + ), + 'expected_error' => null, + ), + 'login matches own email' => array( + 'userdata' => array( + 'user_login' => 'new@example.com', + 'user_email' => 'new@example.com', + ), + 'expected_error' => null, + ), + 'login matches another user email' => array( + 'userdata' => array( + 'user_login' => 'existing@example.com', + 'user_email' => 'new@example.com', + ), + 'expected_error' => 'existing_user_email_as_login', + ), + 'login matches another user email, case' => array( + 'userdata' => array( + 'user_login' => 'Existing@Example.com', + 'user_email' => 'new@example.com', + ), + 'expected_error' => 'existing_user_email_as_login', + ), + 'email matches another user login' => array( + 'userdata' => array( + 'user_login' => 'new', + 'user_email' => 'existing-login@example.com', + ), + 'expected_error' => 'existing_user_login_as_email', + ), + 'email matches another user email' => array( + 'userdata' => array( + 'user_login' => 'new', + 'user_email' => 'existing@example.com', + ), + 'expected_error' => 'existing_user_email', + ), + 'login matches another user login' => array( + 'userdata' => array( + 'user_login' => 'existing', + 'user_email' => 'new@example.com', + ), + 'expected_error' => 'existing_user_login', + ), + ); + } + + /** + * Tests that wp_update_user() only rejects an email address when it is + * changing to another user's login, and never blocks updates to users + * whose existing data already collides. + * + * @ticket 57394 + * @ticket 57967 + * + * @covers ::wp_insert_user + * @covers ::wp_update_user + * + * @dataProvider data_wp_update_user_login_email_collisions + * + * @param array $user_a Login and email for user A, the user being updated. + * @param array $user_b Login and email for user B. + * @param array $update Data to update user A with. + * @param string|null $expected_error Expected error code, or null for success. + */ + public function test_wp_update_user_login_email_collisions( $user_a, $user_b, $update, $expected_error ) { + list( $user_a_id ) = $this->create_users_with_logins_and_emails( $user_a, $user_b ); + + $update['ID'] = $user_a_id; + $result = wp_update_user( $update ); - $user_id = wp_insert_user( + if ( null === $expected_error ) { + $this->assertSame( $user_a_id, $result, 'The user should have been updated.' ); + if ( isset( $update['user_email'] ) ) { + $this->assertSame( $update['user_email'], get_userdata( $user_a_id )->user_email, 'The email address was not updated.' ); + } + } else { + $this->assertWPError( $result, 'A WP_Error object should have been returned.' ); + $this->assertSame( $expected_error, $result->get_error_code(), 'An unexpected error code was returned.' ); + } + } + + /** + * Data provider for test_wp_update_user_login_email_collisions(). + * + * @return array[] + */ + public function data_wp_update_user_login_email_collisions() { + return array( + 'login matches own email, other field changes' => array( + 'user_a' => array( + 'login' => 'a@example.com', + 'email' => 'a@example.com', + ), + 'user_b' => array( + 'login' => 'b', + 'email' => 'b@example.com', + ), + 'update' => array( 'display_name' => 'Updated' ), + 'expected_error' => null, + ), + 'login already matches another user email' => array( + 'user_a' => array( + 'login' => 'b@example.com', + 'email' => 'a@example.com', + ), + 'user_b' => array( + 'login' => 'b', + 'email' => 'b@example.com', + ), + 'update' => array( 'display_name' => 'Updated' ), + 'expected_error' => null, + ), + 'email already matches another user login' => array( + 'user_a' => array( + 'login' => 'a', + 'email' => 'b@example.com', + ), + 'user_b' => array( + 'login' => 'b@example.com', + 'email' => 'b-other@example.com', + ), + 'update' => array( 'display_name' => 'Updated' ), + 'expected_error' => null, + ), + 'email already matches another user login, unchanged email resubmitted' => array( + 'user_a' => array( + 'login' => 'a', + 'email' => 'b@example.com', + ), + 'user_b' => array( + 'login' => 'b@example.com', + 'email' => 'b-other@example.com', + ), + 'update' => array( 'user_email' => 'b@example.com' ), + 'expected_error' => null, + ), + 'email changes to own login' => array( + 'user_a' => array( + 'login' => 'a@example.com', + 'email' => 'a-other@example.com', + ), + 'user_b' => array( + 'login' => 'b', + 'email' => 'b@example.com', + ), + 'update' => array( 'user_email' => 'a@example.com' ), + 'expected_error' => null, + ), + 'email changes to unique address' => array( + 'user_a' => array( + 'login' => 'a', + 'email' => 'a@example.com', + ), + 'user_b' => array( + 'login' => 'b', + 'email' => 'b@example.com', + ), + 'update' => array( 'user_email' => 'a-new@example.com' ), + 'expected_error' => null, + ), + 'email changes to another user login' => array( + 'user_a' => array( + 'login' => 'a', + 'email' => 'a@example.com', + ), + 'user_b' => array( + 'login' => 'b@example.com', + 'email' => 'b-other@example.com', + ), + 'update' => array( 'user_email' => 'b@example.com' ), + 'expected_error' => 'existing_user_login_as_email', + ), + 'email changes to another user email' => array( + 'user_a' => array( + 'login' => 'a', + 'email' => 'a@example.com', + ), + 'user_b' => array( + 'login' => 'b', + 'email' => 'b@example.com', + ), + 'update' => array( 'user_email' => 'b@example.com' ), + 'expected_error' => 'existing_user_email', + ), + ); + } + + /** + * Tests that register_new_user() rejects a login or email that collides + * with another user's email or login. + * + * @ticket 57394 + * + * @covers ::register_new_user + */ + public function test_register_new_user_login_email_collisions() { + $this->create_users_with_logins_and_emails( + array( + 'login' => 'existing', + 'email' => 'existing@example.com', + ), array( - 'user_login' => $new_email, - 'user_email' => $new_email, - 'user_pass' => 'whatever', - 'user_nicename' => 'whatever', + 'login' => 'existing-login@example.com', + 'email' => 'other@example.com', ) ); - $this->assertNotWPError( $user_id ); - $user_id = wp_update_user( + $result = register_new_user( 'existing@example.com', 'new@example.com' ); + $this->assertWPError( $result, 'A login matching another user email should be rejected.' ); + $this->assertContains( 'username_exists_as_email', $result->get_error_codes(), 'The login collision error is missing.' ); + + $result = register_new_user( 'new', 'existing-login@example.com' ); + $this->assertWPError( $result, 'An email matching another user login should be rejected.' ); + $this->assertContains( 'email_exists_as_username', $result->get_error_codes(), 'The email collision error is missing.' ); + } + + /** + * Tests that edit_user() rejects a new user whose login matches another + * user's email address. + * + * @ticket 57394 + * + * @covers ::edit_user + */ + public function test_edit_user_rejects_new_login_that_matches_another_user_email() { + $this->create_users_with_logins_and_emails( + array( + 'login' => 'existing', + 'email' => 'existing@example.com', + ), array( - 'ID' => $user_id, - 'user_login' => $new_email, - 'user_email' => $new_email, - 'user_nicename' => 'new-nicename', + 'login' => 'other', + 'email' => 'other@example.com', ) ); - // No error should be returned. - $this->assertNotWPError( $user_id ); - // The user_nicename should have been updated. - $this->assertSame( 'new-nicename', get_userdata( $user_id )->user_nicename ); + + $_POST = array( + 'user_login' => 'existing@example.com', + 'email' => 'new@example.com', + 'pass1' => 'password', + 'pass2' => 'password', + ); + + $result = edit_user(); + + $this->assertWPError( $result, 'A WP_Error object should have been returned.' ); + $this->assertContains( 'user_login', $result->get_error_codes(), 'The login collision error is missing.' ); + } + + /** + * Tests that edit_user() only rejects an email address matching another + * user's login when the email address is changing. + * + * @ticket 57394 + * + * @covers ::edit_user + */ + public function test_edit_user_checks_only_changed_email_against_logins() { + list( $user_a_id ) = $this->create_users_with_logins_and_emails( + array( + 'login' => 'a', + 'email' => 'b@example.com', + ), + array( + 'login' => 'b@example.com', + 'email' => 'b-other@example.com', + ) + ); + + $user_c_id = self::factory()->user->create( array( 'user_email' => 'c@example.com' ) ); + + wp_set_current_user( self::$admin_id ); + + // User A already collides with user B; saving without changing the email must work. + $_POST = array( + 'email' => 'b@example.com', + 'nickname' => 'a', + ); + $result = edit_user( $user_a_id ); + $this->assertSame( $user_a_id, $result, 'Saving an existing collision without changes should succeed.' ); + + // User C changing their email to user B's login must fail. + $_POST = array( + 'email' => 'b@example.com', + 'nickname' => 'c', + ); + $result = edit_user( $user_c_id ); + $this->assertWPError( $result, 'A WP_Error object should have been returned.' ); + $this->assertContains( 'email_exists_as_username', $result->get_error_codes(), 'The email collision error is missing.' ); + } + + /** + * Tests that a profile email change matching another user's login does not + * send a confirmation email. + * + * @ticket 57394 + * + * @covers ::send_confirmation_on_profile_email + */ + public function test_send_confirmation_on_profile_email_rejects_another_user_login() { + global $errors; + + list( $user_a_id ) = $this->create_users_with_logins_and_emails( + array( + 'login' => 'a', + 'email' => 'a@example.com', + ), + array( + 'login' => 'b@example.com', + 'email' => 'b-other@example.com', + ) + ); + + reset_phpmailer_instance(); + wp_set_current_user( $user_a_id ); + + $errors = new WP_Error(); + $_POST['user_id'] = $user_a_id; + $_POST['email'] = 'b@example.com'; + send_confirmation_on_profile_email(); + + $this->assertFalse( tests_retrieve_phpmailer_instance()->get_sent(), 'A confirmation email should not have been sent.' ); + $this->assertContains( 'user_email', $errors->get_error_codes(), 'The email collision error is missing.' ); } /**