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
13 changes: 2 additions & 11 deletions src/wp-admin/user-edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,12 @@

// Execute confirmed email change. See send_confirmation_on_profile_email().
if ( IS_PROFILE_PAGE && isset( $_GET['newuseremail'] ) && $current_user->ID ) {
$new_email = get_user_meta( $current_user->ID, '_new_email', true );
if ( $new_email && hash_equals( $new_email['hash'], $_GET['newuseremail'] ) ) {
$user = new stdClass();
$user->ID = $current_user->ID;
$user->user_email = esc_html( trim( $new_email['newemail'] ) );
if ( is_multisite() && $wpdb->get_var( $wpdb->prepare( "SELECT user_login FROM {$wpdb->signups} WHERE user_login = %s", $current_user->user_login ) ) ) {
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->signups} SET user_email = %s WHERE user_login = %s", $user->user_email, $current_user->user_login ) );
}
wp_update_user( $user );
delete_user_meta( $current_user->ID, '_new_email' );
if ( confirm_user_email_change( $current_user->ID, $_GET['newuseremail'] ) ) {
wp_redirect( add_query_arg( array( 'updated' => 'true' ), self_admin_url( 'profile.php' ) ) );
die();
} else {
wp_redirect( add_query_arg( array( 'error' => 'new-email' ), self_admin_url( 'profile.php' ) ) );
}
die();
} elseif ( IS_PROFILE_PAGE && ! empty( $_GET['dismiss'] ) && $current_user->ID . '_new_email' === $_GET['dismiss'] ) {
check_admin_referer( 'dismiss-' . $current_user->ID . '_new_email' );
delete_user_meta( $current_user->ID, '_new_email' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,27 @@ public function update_item( $request ) {
}
}

/*
* Ask a user to confirm a change to their own email address, rather than
* applying it immediately, as the profile screen does. The change is held
* in the `_new_email` user meta until it is confirmed.
*/
if ( is_string( $request['email'] ) && '' !== $request['email'] ) {
$email_sent = send_user_email_change_confirmation_email( $user, $request['email'] );

if ( is_wp_error( $email_sent ) ) {
return new WP_Error(
'rest_user_invalid_email',
__( 'Invalid email address.' ),
array( 'status' => 400 )
);
}

if ( true === $email_sent ) {
unset( $request['email'] );
}
}

$user = $this->prepare_item_for_database( $request );

// Ensure we're operating on the same user we already checked.
Expand Down
247 changes: 187 additions & 60 deletions src/wp-includes/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -3886,46 +3886,117 @@ function send_confirmation_on_profile_email( $user_id = 0 ) {
return false;
}

if ( $current_user->user_email !== $_POST['email'] ) {
if ( ! is_email( $_POST['email'] ) ) {
$email_sent = send_user_email_change_confirmation_email( $current_user, $_POST['email'] );

if ( is_wp_error( $email_sent ) ) {
// WP_Error::copy_errors() with the addition of adding data.
foreach ( $email_sent->get_error_codes() as $code ) {
$errors->add(
'user_email',
__( '<strong>Error:</strong> The email address is not correct.' ),
$code,
$email_sent->get_error_message( $code ),
array(
'form-field' => 'email',
)
);

$_POST['email'] = addslashes( $current_user->user_email );
return;
}

if ( email_exists( $_POST['email'] ) ) {
$errors->add(
'user_email',
__( '<strong>Error:</strong> The email address is already used.' ),
array(
'form-field' => 'email',
)
);
delete_user_meta( $current_user->ID, '_new_email' );
$_POST['email'] = addslashes( $current_user->user_email );
return;
}

$_POST['email'] = addslashes( $current_user->user_email );
return;
}
if ( true === $email_sent ) {
$_POST['email'] = $current_user->user_email;
}
}

$hash = md5( $_POST['email'] . time() . wp_rand() );
$new_user_email = array(
'hash' => $hash,
'newemail' => $_POST['email'],
/**
* Sends a confirmation request email when a change of user email address is attempted.
*
* The new address is held in the `_new_email` user meta until the user confirms it
* by following the link in the email. The address on the account is unchanged
* until then.
*
* A confirmation is only required when a user changes their own email address. An
* administrator changing somebody else's address is not asked to confirm it.
*
* @since 7.2.0
*
* @param WP_User $user The user whose email address is being changed.
* @param string $email The new email address.
* @return true|WP_Error|null True if a confirmation email was sent and the change should not be
* applied yet, WP_Error if the address was rejected, null if no
* confirmation is needed and the change may be applied.
*/
function send_user_email_change_confirmation_email( $user, $email ) {
if ( ! $user instanceof WP_User || ! $user->exists() ) {
return null;
}

/*
* Correcting the case of an address is the same mailbox, so it is not a change
* that needs confirming, and `email_exists()` below would read it as an address
* that is already in use. See #44672.
*/
if ( 0 === strcasecmp( $user->user_email, $email ) ) {
return null;
}

if ( ! is_email( $email ) ) {
return new WP_Error(
'user_email',
__( '<strong>Error:</strong> The email address is not correct.' )
);
update_user_meta( $current_user->ID, '_new_email', $new_user_email );
}

if ( email_exists( $email ) ) {
delete_user_meta( $user->ID, '_new_email' );

return new WP_Error(
'user_email',
__( '<strong>Error:</strong> The email address is already used.' )
);
}

// The email is only sent if a user changes their own email.
$should_send_email_for_change = ( get_current_user_id() === $user->ID );

/**
* Filters whether a 'confirm your email address' email should be sent.
* If false is returned, the change is made immediately.
*
* @since 7.2.0
*
* @param bool $should_send_email_for_change Whether to use an email confirmation.
* @param WP_User $user The user having their email changed.
* @param string $email The new email address.
*/
$should_send_email_for_change = apply_filters( 'should_send_email_for_email_change', $should_send_email_for_change, $user, $email );

$sitename = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
if ( ! $should_send_email_for_change ) {
return null;
}

/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
$email_text = __(
'Howdy ###USERNAME###,
$hash = md5( $email . time() . wp_rand() );
$new_user_email = array(
'hash' => $hash,
'newemail' => $email,
);
update_user_meta( $user->ID, '_new_email', $new_user_email );

$confirm_url = add_query_arg(
array(
'action' => 'confirmemail',
'id' => $user->ID,
'hash' => $hash,
),
wp_login_url()
);

$sitename = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );

/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
$email_text = __(
'Howdy ###USERNAME###,

You recently requested to have the email address on your account changed.

Expand All @@ -3940,43 +4011,99 @@ function send_confirmation_on_profile_email( $user_id = 0 ) {
Regards,
All at ###SITENAME###
###SITEURL###'
);
);

/**
* Filters the text of the email sent when a change of user email address is attempted.
*
* The following strings have a special meaning and will get replaced dynamically:
*
* - `###USERNAME###` The current user's username.
* - `###ADMIN_URL###` The link to click on to confirm the email change.
* - `###EMAIL###` The new email.
* - `###SITENAME###` The name of the site.
* - `###SITEURL###` The URL to the site.
*
* @since MU (3.0.0)
* @since 4.9.0 This filter is no longer Multisite specific.
*
* @param string $email_text Text in the email.
* @param array $new_user_email {
* Data relating to the new user email address.
*
* @type string $hash The secure hash used in the confirmation link URL.
* @type string $newemail The proposed new email address.
* }
*/
$content = apply_filters( 'new_user_email_content', $email_text, $new_user_email );
/**
* Filters the text of the email sent when a change of user email address is attempted.
*
* The following strings have a special meaning and will get replaced dynamically:
*
* - `###USERNAME###` The current user's username.
* - `###ADMIN_URL###` The link to click on to confirm the email change.
* - `###EMAIL###` The new email.
* - `###SITENAME###` The name of the site.
* - `###SITEURL###` The URL to the site.
*
* @since MU (3.0.0)
* @since 4.9.0 This filter is no longer Multisite specific.
*
* @param string $email_text Text in the email.
* @param array $new_user_email {
* Data relating to the new user email address.
*
* @type string $hash The secure hash used in the confirmation link URL.
* @type string $newemail The proposed new email address.
* }
*/
$content = apply_filters( 'new_user_email_content', $email_text, $new_user_email );

$content = str_replace( '###USERNAME###', $current_user->user_login, $content );
$content = str_replace( '###ADMIN_URL###', esc_url( self_admin_url( 'profile.php?newuseremail=' . $hash ) ), $content );
$content = str_replace( '###EMAIL###', $_POST['email'], $content );
$content = str_replace( '###SITENAME###', $sitename, $content );
$content = str_replace( '###SITEURL###', home_url(), $content );
$content = str_replace( '###USERNAME###', $user->user_login, $content );
$content = str_replace( '###ADMIN_URL###', esc_url( $confirm_url ), $content );
$content = str_replace( '###EMAIL###', $email, $content );
$content = str_replace( '###SITENAME###', $sitename, $content );
$content = str_replace( '###SITEURL###', home_url(), $content );

/* translators: New email address notification email subject. %s: Site title. */
wp_mail( $_POST['email'], sprintf( __( '[%s] Email Change Request' ), $sitename ), $content );
/* translators: New email address notification email subject. %s: Site title. */
wp_mail( $email, sprintf( __( '[%s] Email Change Request' ), $sitename ), $content );

$_POST['email'] = $current_user->user_email;
return true;
}

/**
* Applies a pending email address change once the user has confirmed it.
*
* @since 7.2.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int $user_id The ID of the user whose email address is being changed.
* @param string $email_hash The confirmation hash from the link in the confirmation email.
* @return bool Whether the email address was changed.
*/
function confirm_user_email_change( $user_id, $email_hash ) {
global $wpdb;

$the_user = get_userdata( $user_id );

if ( ! $the_user ) {
return false;
}

$new_email = get_user_meta( $the_user->ID, '_new_email', true );

if ( ! is_array( $new_email ) || empty( $new_email['hash'] ) || empty( $new_email['newemail'] ) ) {
return false;
}

if ( ! hash_equals( $new_email['hash'], (string) $email_hash ) ) {
return false;
}

$user = new stdClass();
$user->ID = $the_user->ID;
$user->user_email = esc_html( trim( $new_email['newemail'] ) );
if ( is_multisite() && $wpdb->get_var( $wpdb->prepare( "SELECT user_login FROM {$wpdb->signups} WHERE user_login = %s", $the_user->user_login ) ) ) {
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->signups} SET user_email = %s WHERE user_login = %s", $user->user_email, $the_user->user_login ) );
}

$updated = wp_update_user( $user );

if ( is_wp_error( $updated ) ) {
return false;
}

delete_user_meta( $user->ID, '_new_email' );

/**
* Fires after a user has confirmed a change to their email address.
*
* @since 7.2.0
*
* @param int $user_id The ID of the user whose email address was changed.
*/
do_action( 'user_email_confirmed', $user->ID );

return true;
}

/**
Expand Down
39 changes: 39 additions & 0 deletions src/wp-login.php
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ function wp_login_viewport_meta() {
'register',
'checkemail',
'confirmaction',
'confirmemail',
'login',
WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED,
);
Expand Down Expand Up @@ -1237,6 +1238,44 @@ function wp_login_viewport_meta() {
login_footer();
break;

case 'confirmemail':
if ( ! isset( $_GET['id'], $_GET['hash'] ) ) {
wp_die( __( 'Missing or invalid key.' ) );
}

$user_id = (int) $_GET['id'];
$email_key = sanitize_text_field( wp_unslash( $_GET['hash'] ) );

if ( ! is_user_logged_in() ) {
$confirm_url = add_query_arg(
array(
'action' => 'confirmemail',
'id' => $user_id,
'hash' => rawurlencode( $email_key ),
),
wp_login_url()
);

wp_safe_redirect( wp_login_url( $confirm_url ) );
exit;
}

if ( ! current_user_can( 'edit_user', $user_id ) ) {
wp_die( __( 'Missing or invalid key.' ) );
}

if ( ! confirm_user_email_change( $user_id, $email_key ) ) {
wp_die( __( 'Missing or invalid key.' ) );
}

login_header(
__( 'Email address confirmed.' ),
'<p class="message">' . __( 'Your new email address has been confirmed.' ) . '</p>'
);

login_footer();
exit;

case 'confirmaction':
if ( ! isset( $_GET['request_id'] ) ) {
wp_die( __( 'Missing request ID.' ) );
Expand Down
2 changes: 1 addition & 1 deletion tests/phpstan/baselines/variable.undefined.neon
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ parameters:
-
message: '#^Variable \$wpdb might not be defined\.$#'
identifier: variable.undefined
count: 12
count: 6
path: ../../../src/wp-admin/user-edit.php
-
message: '#^Variable \$blog_id might not be defined\.$#'
Expand Down
Loading
Loading