From 021a33ab8a671ee7c15b52c457c3c82321fa4a42 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 9 Apr 2026 05:32:41 +0000 Subject: [PATCH 1/4] Fix for retaining user info in user edit after submit --- src/wp-admin/includes/user.php | 116 +++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index 477ee9b5af4b7..b91899860d8a8 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -226,6 +226,14 @@ function edit_user( $user_id = 0 ) { do_action_ref_array( 'user_profile_update_errors', array( &$errors, $update, &$user ) ); if ( $errors->has_errors() ) { + if ( $update ) { + $updated_user_id = _edit_user_partial_update( $user, $errors ); + + if ( is_wp_error( $updated_user_id ) ) { + $errors->merge_from( $updated_user_id ); + } + } + return $errors; } @@ -249,6 +257,114 @@ function edit_user( $user_id = 0 ) { return $user_id; } +/** + * Updates a user with the valid data from a failed profile submission. + * + * When an existing user update fails due to field-specific validation errors, + * the remaining valid fields can still be saved so they are not lost on the + * subsequent page load. + * + * @since 7.1.0 + * @access private + * + * @param stdClass $user User data prepared for `wp_update_user()`. + * @param WP_Error $errors Validation errors from `edit_user()`. + * @return int|WP_Error User ID on success, WP_Error on failure, or 0 if no partial update was attempted. + */ +function _edit_user_partial_update( $user, $errors ) { + $error_fields = _get_edit_user_error_fields( $errors ); + + if ( ! $error_fields ) { + return 0; + } + + foreach ( $error_fields as $field ) { + $property = _get_edit_user_property( $field ); + + if ( property_exists( $user, $property ) ) { + unset( $user->$property ); + } + } + + if ( 1 === count( get_object_vars( $user ) ) ) { + return 0; + } + + return wp_update_user( $user ); +} + +/** + * Maps edit-user form fields to the properties stored by `wp_update_user()`. + * + * @since 7.1.0 + * @access private + * + * @param string $field Form field name. + * @return string User property name. + */ +function _get_edit_user_property( $field ) { + switch ( $field ) { + case 'email': + return 'user_email'; + case 'url': + return 'user_url'; + case 'pass1': + case 'pass2': + return 'user_pass'; + default: + return $field; + } +} + +/** + * Gets the form fields responsible for a failed user update. + * + * Only errors that can be mapped back to a specific submitted field are + * considered safe for partial saving. Any unknown error aborts the partial + * update so the submission remains fully blocked. + * + * @since 7.1.0 + * @access private + * + * @param WP_Error $errors Validation errors from `edit_user()`. + * @return string[] Array of field names that should be excluded from the save. + */ +function _get_edit_user_error_fields( $errors ) { + $error_fields = array(); + + foreach ( $errors->get_error_codes() as $code ) { + $mapped_fields = array(); + + foreach ( $errors->get_all_error_data( $code ) as $data ) { + if ( is_array( $data ) && ! empty( $data['form-field'] ) ) { + $mapped_fields[] = $data['form-field']; + } + } + + if ( empty( $mapped_fields ) ) { + switch ( $code ) { + case 'invalid_username': + case 'user_login': + $mapped_fields[] = 'user_login'; + break; + case 'nickname': + $mapped_fields[] = 'nickname'; + break; + } + } + + if ( empty( $mapped_fields ) ) { + return array(); + } + + foreach ( $mapped_fields as $field ) { + $error_fields[ $field ] = true; + } + } + + return array_keys( $error_fields ); +} + /** * Fetch a filtered list of user roles that the current user is * allowed to edit. From 5a9766f4b1d134a685b924202214ebe9d153bdfe Mon Sep 17 00:00:00 2001 From: root Date: Thu, 9 Apr 2026 08:20:18 +0000 Subject: [PATCH 2/4] Unit test cases fixes --- src/wp-admin/includes/user.php | 187 +++++++++++------- src/wp-admin/user-edit.php | 28 ++- ...cludes_User_GetUserToEditFromPost_Test.php | 116 +++++++++++ 3 files changed, 258 insertions(+), 73 deletions(-) create mode 100644 tests/phpunit/tests/admin/Admin_Includes_User_GetUserToEditFromPost_Test.php diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index b91899860d8a8..1ea38f63aa03f 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -226,14 +226,6 @@ function edit_user( $user_id = 0 ) { do_action_ref_array( 'user_profile_update_errors', array( &$errors, $update, &$user ) ); if ( $errors->has_errors() ) { - if ( $update ) { - $updated_user_id = _edit_user_partial_update( $user, $errors ); - - if ( is_wp_error( $updated_user_id ) ) { - $errors->merge_from( $updated_user_id ); - } - } - return $errors; } @@ -257,77 +249,17 @@ function edit_user( $user_id = 0 ) { return $user_id; } -/** - * Updates a user with the valid data from a failed profile submission. - * - * When an existing user update fails due to field-specific validation errors, - * the remaining valid fields can still be saved so they are not lost on the - * subsequent page load. - * - * @since 7.1.0 - * @access private - * - * @param stdClass $user User data prepared for `wp_update_user()`. - * @param WP_Error $errors Validation errors from `edit_user()`. - * @return int|WP_Error User ID on success, WP_Error on failure, or 0 if no partial update was attempted. - */ -function _edit_user_partial_update( $user, $errors ) { - $error_fields = _get_edit_user_error_fields( $errors ); - - if ( ! $error_fields ) { - return 0; - } - - foreach ( $error_fields as $field ) { - $property = _get_edit_user_property( $field ); - - if ( property_exists( $user, $property ) ) { - unset( $user->$property ); - } - } - - if ( 1 === count( get_object_vars( $user ) ) ) { - return 0; - } - - return wp_update_user( $user ); -} - -/** - * Maps edit-user form fields to the properties stored by `wp_update_user()`. - * - * @since 7.1.0 - * @access private - * - * @param string $field Form field name. - * @return string User property name. - */ -function _get_edit_user_property( $field ) { - switch ( $field ) { - case 'email': - return 'user_email'; - case 'url': - return 'user_url'; - case 'pass1': - case 'pass2': - return 'user_pass'; - default: - return $field; - } -} - /** * Gets the form fields responsible for a failed user update. * - * Only errors that can be mapped back to a specific submitted field are - * considered safe for partial saving. Any unknown error aborts the partial - * update so the submission remains fully blocked. + * These fields are excluded when rebuilding the edit form from submitted data + * after a failed update. * * @since 7.1.0 * @access private * * @param WP_Error $errors Validation errors from `edit_user()`. - * @return string[] Array of field names that should be excluded from the save. + * @return string[] Array of field names that should not be repopulated. */ function _get_edit_user_error_fields( $errors ) { $error_fields = array(); @@ -365,6 +297,119 @@ function _get_edit_user_error_fields( $errors ) { return array_keys( $error_fields ); } +/** + * Retrieves user data for the edit screen and overlays safe submitted values. + * + * When an existing user update fails validation, the edit and profile screens + * should preserve submitted values without writing them to the database. Fields + * that produced errors are left at their stored values, and password fields are + * never repopulated. + * + * @since 7.1.0 + * @access private + * + * @param int $user_id User ID. + * @param WP_Error $errors Validation errors from `edit_user()`. + * @return WP_User|false WP_User object on success, false on failure. + */ +function _get_user_to_edit_from_post( $user_id, $errors ) { + $user = get_user_to_edit( $user_id ); + $is_profile_page = defined( 'IS_PROFILE_PAGE' ) && IS_PROFILE_PAGE; + + if ( ! $user || ! is_wp_error( $errors ) || ! $errors->has_errors() ) { + return $user; + } + + $error_fields = array_flip( _get_edit_user_error_fields( $errors ) ); + $properties = array( + 'first_name' => 'first_name', + 'last_name' => 'last_name', + 'nickname' => 'nickname', + 'display_name' => 'display_name', + 'email' => 'user_email', + 'url' => 'user_url', + 'description' => 'description', + 'locale' => 'locale', + 'admin_color' => 'admin_color', + ); + + foreach ( $properties as $field => $property ) { + if ( isset( $error_fields[ $field ] ) || ! isset( $_POST[ $field ] ) ) { + continue; + } + + $user->$property = wp_unslash( $_POST[ $field ] ); + } + + foreach ( wp_get_user_contact_methods( $user ) as $method => $label ) { + if ( isset( $error_fields[ $method ] ) || ! isset( $_POST[ $method ] ) ) { + continue; + } + + $user->$method = wp_unslash( $_POST[ $method ] ); + } + + if ( ! isset( $error_fields['rich_editing'] ) ) { + $user->rich_editing = isset( $_POST['rich_editing'] ) && 'false' === $_POST['rich_editing'] ? 'false' : 'true'; + } + + if ( ! isset( $error_fields['syntax_highlighting'] ) ) { + $user->syntax_highlighting = isset( $_POST['syntax_highlighting'] ) && 'false' === $_POST['syntax_highlighting'] ? 'false' : 'true'; + } + + if ( ! isset( $error_fields['comment_shortcuts'] ) ) { + $user->comment_shortcuts = isset( $_POST['comment_shortcuts'] ) && 'true' === $_POST['comment_shortcuts'] ? 'true' : ''; + } + + if ( ! isset( $error_fields['admin_bar_front'] ) ) { + $user->show_admin_bar_front = isset( $_POST['admin_bar_front'] ) ? 'true' : 'false'; + } + + if ( ! isset( $error_fields['use_ssl'] ) ) { + $user->use_ssl = ! empty( $_POST['use_ssl'] ) ? 1 : 0; + } + + if ( ! isset( $error_fields['role'] ) + && ! $is_profile_page + && ! is_network_admin() + && isset( $_POST['role'] ) + && current_user_can( 'promote_user', $user_id ) + ) { + $role = sanitize_text_field( wp_unslash( $_POST['role'] ) ); + $editable_roles = get_editable_roles(); + + if ( '' === $role || ! empty( $editable_roles[ $role ] ) ) { + $user->roles = $role ? array( $role ) : array(); + } + } + + return $user; +} + +/** + * Filters user options for the edit screen when repopulating submitted values. + * + * @since 7.1.0 + * @access private + * + * @param mixed $result Value for the user's option. + * @param string $option Name of the option being retrieved. + * @param WP_User $user WP_User object of the user whose option is being retrieved. + * @return mixed Filtered user option value. + */ +function _get_user_edit_form_posted_option( $result, $option, $user ) { + global $_wp_user_edit_posted_options; + + if ( empty( $_wp_user_edit_posted_options['user_id'] ) + || (int) $_wp_user_edit_posted_options['user_id'] !== (int) $user->ID + || ! isset( $_wp_user_edit_posted_options['options'][ $option ] ) + ) { + return $result; + } + + return $_wp_user_edit_posted_options['options'][ $option ]; +} + /** * Fetch a filtered list of user roles that the current user is * allowed to edit. diff --git a/src/wp-admin/user-edit.php b/src/wp-admin/user-edit.php index cfad6afbab8dc..40969a00005b5 100644 --- a/src/wp-admin/user-edit.php +++ b/src/wp-admin/user-edit.php @@ -189,7 +189,11 @@ // Intentional fall-through to display $errors. default: - $profile_user = get_user_to_edit( $user_id ); + if ( isset( $errors ) && is_wp_error( $errors ) ) { + $profile_user = _get_user_to_edit_from_post( $user_id, $errors ); + } else { + $profile_user = get_user_to_edit( $user_id ); + } if ( ! current_user_can( 'edit_user', $user_id ) ) { wp_die( __( 'Sorry, you are not allowed to edit this user.' ) ); @@ -336,6 +340,16 @@ admin_color ) ) { + $_wp_user_edit_posted_options = array( + 'user_id' => $profile_user->ID, + 'options' => array( + 'admin_color' => $profile_user->admin_color, + ), + ); + add_filter( 'get_user_option_admin_color', '_get_user_edit_form_posted_option', 10, 3 ); + } + /** * Fires in the 'Administration Color Scheme' section of the user editing screen. * @@ -348,6 +362,11 @@ * @param int $user_id The user ID. */ do_action( 'admin_color_scheme_picker', $user_id ); + + if ( isset( $_wp_user_edit_posted_options ) ) { + remove_filter( 'get_user_option_admin_color', '_get_user_edit_form_posted_option', 10 ); + unset( $_wp_user_edit_posted_options ); + } ?> @@ -369,8 +388,13 @@ + show_admin_bar_front + : _get_admin_bar_pref( 'front', $profile_user->ID ); + ?>
diff --git a/tests/phpunit/tests/admin/Admin_Includes_User_GetUserToEditFromPost_Test.php b/tests/phpunit/tests/admin/Admin_Includes_User_GetUserToEditFromPost_Test.php new file mode 100644 index 0000000000000..42206ed4bab65 --- /dev/null +++ b/tests/phpunit/tests/admin/Admin_Includes_User_GetUserToEditFromPost_Test.php @@ -0,0 +1,116 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + $user_id = self::factory()->user->create( + array( + 'role' => 'subscriber', + 'user_email' => 'stored@example.com', + 'first_name' => 'Stored', + 'description' => 'Stored bio', + 'rich_editing' => 'true', + ) + ); + + wp_set_current_user( $administrator_id ); + update_user_option( $user_id, 'admin_color', 'modern' ); + update_user_option( $user_id, 'show_admin_bar_front', 'false' ); + + $_POST = array( + 'first_name' => 'Posted', + 'display_name' => 'Posted Display Name', + 'email' => 'not-an-email-address', + 'description' => 'Posted bio', + 'rich_editing' => 'false', + 'syntax_highlighting' => 'false', + 'comment_shortcuts' => 'true', + 'admin_bar_front' => '1', + 'admin_color' => 'midnight', + 'use_ssl' => '1', + 'role' => 'editor', + 'pass1' => 'secret-pass', + 'pass2' => '', + ); + + $errors = new WP_Error(); + $errors->add( 'invalid_email', 'Invalid email.', array( 'form-field' => 'email' ) ); + $errors->add( 'pass', 'Passwords do not match.', array( 'form-field' => 'pass1' ) ); + + $user = _get_user_to_edit_from_post( $user_id, $errors ); + + $this->assertSame( 'Posted', $user->first_name ); + $this->assertSame( 'Posted Display Name', $user->display_name ); + $this->assertSame( 'Stored bio', get_userdata( $user_id )->description ); + $this->assertSame( 'Posted bio', $user->description ); + $this->assertSame( 'stored@example.com', $user->user_email ); + $this->assertSame( 'false', $user->rich_editing ); + $this->assertSame( 'false', $user->syntax_highlighting ); + $this->assertSame( 'true', $user->comment_shortcuts ); + $this->assertSame( 'true', $user->show_admin_bar_front ); + $this->assertSame( '1', $user->use_ssl ); + $this->assertSame( array( 'editor' ), $user->roles ); + + $stored_user = get_userdata( $user_id ); + + $this->assertSame( 'Stored', $stored_user->first_name ); + $this->assertSame( 'Stored bio', $stored_user->description ); + $this->assertSame( array( 'subscriber' ), $stored_user->roles ); + $this->assertSame( 'modern', get_user_option( 'admin_color', $user_id ) ); + $this->assertFalse( _get_admin_bar_pref( 'front', $user_id ) ); + } + + /** + * Tests that posted user options are only used for the matching user. + * + * @ticket 26962 + */ + public function test_get_user_edit_form_posted_option_only_overrides_matching_user() { + $user_id = self::factory()->user->create(); + $other_user_id = self::factory()->user->create(); + + $GLOBALS['_wp_user_edit_posted_options'] = array( + 'user_id' => $user_id, + 'options' => array( + 'admin_color' => 'midnight', + ), + ); + + $this->assertSame( + 'midnight', + _get_user_edit_form_posted_option( 'modern', 'admin_color', get_userdata( $user_id ) ) + ); + $this->assertSame( + 'modern', + _get_user_edit_form_posted_option( 'modern', 'admin_color', get_userdata( $other_user_id ) ) + ); + } +} From 5ef29436f6127fa4cd16f21ec6d33b3b7805c1d7 Mon Sep 17 00:00:00 2001 From: Yash Yadav Date: Thu, 9 Apr 2026 08:51:45 +0000 Subject: [PATCH 3/4] Triggering Unit test cases automation From b3537ae9d2372cf9d6490f191cdb54943289227e Mon Sep 17 00:00:00 2001 From: Yash Yadav Date: Thu, 9 Apr 2026 09:55:06 +0000 Subject: [PATCH 4/4] Triggering Unit test cases automation