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
161 changes: 161 additions & 0 deletions src/wp-admin/includes/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,167 @@ function edit_user( $user_id = 0 ) {
return $user_id;
}

/**
* Gets the form fields responsible for a failed user update.
*
* 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 not be repopulated.
*/
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 );
}

/**
* 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.
Expand Down
28 changes: 26 additions & 2 deletions src/wp-admin/user-edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.' ) );
Expand Down Expand Up @@ -336,6 +340,16 @@
<th scope="row"><?php _e( 'Administration Color Scheme' ); ?></th>
<td>
<?php
if ( isset( $errors ) && is_wp_error( $errors ) && isset( $profile_user->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.
*
Expand All @@ -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 );
}
?>
</td>
</tr>
Expand All @@ -369,8 +388,13 @@
<tr class="show-admin-bar user-admin-bar-front-wrap">
<th scope="row"><?php _e( 'Toolbar' ); ?></th>
<td>
<?php
$show_admin_bar_front = isset( $errors ) && is_wp_error( $errors )
? 'true' === $profile_user->show_admin_bar_front
: _get_admin_bar_pref( 'front', $profile_user->ID );
?>
<label for="admin_bar_front">
<input name="admin_bar_front" type="checkbox" id="admin_bar_front" value="1"<?php checked( _get_admin_bar_pref( 'front', $profile_user->ID ) ); ?> />
<input name="admin_bar_front" type="checkbox" id="admin_bar_front" value="1"<?php checked( $show_admin_bar_front ); ?> />
<?php _e( 'Show Toolbar when viewing site' ); ?>
</label><br />
</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

/**
* @group admin
* @group user
*
* @covers ::_get_user_to_edit_from_post
* @covers ::_get_edit_user_error_fields
* @covers ::_get_user_edit_form_posted_option
*/
class Admin_Includes_User_GetUserToEditFromPost_Test extends WP_UnitTestCase {

/**
* Cleans up globals after each test.
*/
public function tear_down() {
$_POST = array();
unset( $GLOBALS['_wp_user_edit_posted_options'] );

parent::tear_down();
}

/**
* Tests that safe submitted values are repopulated without changing stored data.
*
* @ticket 26962
*/
public function test_get_user_to_edit_from_post_repopulates_safe_values_without_saving() {
$administrator_id = self::factory()->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 ) )
);
}
}
Loading