Skip to content

Commit 34ee8c9

Browse files
committed
Users: Require a confirmation link in an email to be clicked when a user attempts to change their email address.
This adds this previously Multisite-only functionality to single site installations too. This change prevents accidental or erroneous email address changes from potentially locking users out of their account. Props rodrigosprimo, tharsheblows, johnbillion Fixes #16470 git-svn-id: https://develop.svn.wordpress.org/trunk@41163 602fd350-edb4-49c9-b593-d223f7449a82
1 parent b4d81bd commit 34ee8c9

6 files changed

Lines changed: 184 additions & 109 deletions

File tree

src/wp-admin/includes/admin-filters.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,12 @@
9999
add_action( 'admin_init', 'default_password_nag_handler' );
100100

101101
add_action( 'admin_notices', 'default_password_nag' );
102+
add_action( 'admin_notices', 'new_user_email_admin_notice' );
102103

103104
add_action( 'profile_update', 'default_password_nag_edit_user', 10, 2 );
104105

106+
add_action( 'personal_options_update', 'send_confirmation_on_profile_email' );
107+
105108
// Update hooks.
106109
add_action( 'load-plugins.php', 'wp_plugin_update_rows', 20 ); // After wp_update_plugins() is called.
107110
add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called.

src/wp-admin/includes/ms-admin-filters.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,12 @@
1111
add_filter( 'wp_handle_upload_prefilter', 'check_upload_size' );
1212

1313
// User Hooks
14-
add_action( 'admin_notices', 'new_user_email_admin_notice' );
1514
add_action( 'user_admin_notices', 'new_user_email_admin_notice' );
1615

1716
add_action( 'admin_page_access_denied', '_access_denied_splash', 99 );
1817

1918
add_action( 'add_option_new_admin_email', 'update_option_new_admin_email', 10, 2 );
2019

21-
add_action( 'personal_options_update', 'send_confirmation_on_profile_email' );
22-
2320
add_action( 'update_option_new_admin_email', 'update_option_new_admin_email', 10, 2 );
2421

2522
// Site Hooks.

src/wp-admin/includes/ms.php

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -335,109 +335,6 @@ function update_option_new_admin_email( $old_value, $value ) {
335335
}
336336
}
337337

338-
/**
339-
* Sends an email when an email address change is requested.
340-
*
341-
* @since 3.0.0
342-
*
343-
* @global WP_Error $errors WP_Error object.
344-
* @global wpdb $wpdb WordPress database object.
345-
*/
346-
function send_confirmation_on_profile_email() {
347-
global $errors, $wpdb;
348-
$current_user = wp_get_current_user();
349-
if ( ! is_object($errors) )
350-
$errors = new WP_Error();
351-
352-
if ( $current_user->ID != $_POST['user_id'] )
353-
return false;
354-
355-
if ( $current_user->user_email != $_POST['email'] ) {
356-
if ( !is_email( $_POST['email'] ) ) {
357-
$errors->add( 'user_email', __( "<strong>ERROR</strong>: The email address isn&#8217;t correct." ), array( 'form-field' => 'email' ) );
358-
return;
359-
}
360-
361-
if ( $wpdb->get_var( $wpdb->prepare( "SELECT user_email FROM {$wpdb->users} WHERE user_email=%s", $_POST['email'] ) ) ) {
362-
$errors->add( 'user_email', __( "<strong>ERROR</strong>: The email address is already used." ), array( 'form-field' => 'email' ) );
363-
delete_user_meta( $current_user->ID, '_new_email' );
364-
return;
365-
}
366-
367-
$hash = md5( $_POST['email'] . time() . mt_rand() );
368-
$new_user_email = array(
369-
'hash' => $hash,
370-
'newemail' => $_POST['email']
371-
);
372-
update_user_meta( $current_user->ID, '_new_email', $new_user_email );
373-
374-
$switched_locale = switch_to_locale( get_user_locale() );
375-
376-
/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
377-
$email_text = __( 'Howdy ###USERNAME###,
378-
379-
You recently requested to have the email address on your account changed.
380-
381-
If this is correct, please click on the following link to change it:
382-
###ADMIN_URL###
383-
384-
You can safely ignore and delete this email if you do not want to
385-
take this action.
386-
387-
This email has been sent to ###EMAIL###
388-
389-
Regards,
390-
All at ###SITENAME###
391-
###SITEURL###' );
392-
393-
/**
394-
* Filters the email text sent when a user changes emails.
395-
*
396-
* The following strings have a special meaning and will get replaced dynamically:
397-
* ###USERNAME### The current user's username.
398-
* ###ADMIN_URL### The link to click on to confirm the email change.
399-
* ###EMAIL### The new email.
400-
* ###SITENAME### The name of the site.
401-
* ###SITEURL### The URL to the site.
402-
*
403-
* @since MU
404-
*
405-
* @param string $email_text Text in the email.
406-
* @param string $new_user_email New user email that the current user has changed to.
407-
*/
408-
$content = apply_filters( 'new_user_email_content', $email_text, $new_user_email );
409-
410-
$content = str_replace( '###USERNAME###', $current_user->user_login, $content );
411-
$content = str_replace( '###ADMIN_URL###', esc_url( self_admin_url( 'profile.php?newuseremail=' . $hash ) ), $content );
412-
$content = str_replace( '###EMAIL###', $_POST['email'], $content);
413-
$content = str_replace( '###SITENAME###', wp_specialchars_decode( get_site_option( 'site_name' ), ENT_QUOTES ), $content );
414-
$content = str_replace( '###SITEURL###', network_home_url(), $content );
415-
416-
wp_mail( $_POST['email'], sprintf( __( '[%s] New Email Address' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ), $content );
417-
$_POST['email'] = $current_user->user_email;
418-
419-
if ( $switched_locale ) {
420-
restore_previous_locale();
421-
}
422-
}
423-
}
424-
425-
/**
426-
* Adds an admin notice alerting the user to check for confirmation email
427-
* after email address change.
428-
*
429-
* @since 3.0.0
430-
*
431-
* @global string $pagenow
432-
*/
433-
function new_user_email_admin_notice() {
434-
global $pagenow;
435-
if ( 'profile.php' === $pagenow && isset( $_GET['updated'] ) && $email = get_user_meta( get_current_user_id(), '_new_email', true ) ) {
436-
/* translators: %s: New email address */
437-
echo '<div class="notice notice-info"><p>' . sprintf( __( 'Your email address has not been updated yet. Please check your inbox at %s for a confirmation email.' ), '<code>' . esc_html( $email['newemail'] ) . '</code>' ) . '</p></div>';
438-
}
439-
}
440-
441338
/**
442339
* Check whether a site has used its allotted upload space.
443340
*

src/wp-admin/user-edit.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@
8888
}
8989

9090
// Execute confirmed email change. See send_confirmation_on_profile_email().
91-
if ( is_multisite() && IS_PROFILE_PAGE && isset( $_GET[ 'newuseremail' ] ) && $current_user->ID ) {
91+
if ( IS_PROFILE_PAGE && isset( $_GET[ 'newuseremail' ] ) && $current_user->ID ) {
9292
$new_email = get_user_meta( $current_user->ID, '_new_email', true );
9393
if ( $new_email && hash_equals( $new_email[ 'hash' ], $_GET[ 'newuseremail' ] ) ) {
9494
$user = new stdClass;
9595
$user->ID = $current_user->ID;
9696
$user->user_email = esc_html( trim( $new_email[ 'newemail' ] ) );
97-
if ( $wpdb->get_var( $wpdb->prepare( "SELECT user_login FROM {$wpdb->signups} WHERE user_login = %s", $current_user->user_login ) ) ) {
97+
if ( is_multisite() && $wpdb->get_var( $wpdb->prepare( "SELECT user_login FROM {$wpdb->signups} WHERE user_login = %s", $current_user->user_login ) ) ) {
9898
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->signups} SET user_email = %s WHERE user_login = %s", $user->user_email, $current_user->user_login ) );
9999
}
100100
wp_update_user( $user );
@@ -104,7 +104,7 @@
104104
} else {
105105
wp_redirect( add_query_arg( array( 'error' => 'new-email' ), self_admin_url( 'profile.php' ) ) );
106106
}
107-
} elseif ( is_multisite() && IS_PROFILE_PAGE && !empty( $_GET['dismiss'] ) && $current_user->ID . '_new_email' === $_GET['dismiss'] ) {
107+
} elseif ( IS_PROFILE_PAGE && ! empty( $_GET['dismiss'] ) && $current_user->ID . '_new_email' === $_GET['dismiss'] ) {
108108
check_admin_referer( 'dismiss-' . $current_user->ID . '_new_email' );
109109
delete_user_meta( $current_user->ID, '_new_email' );
110110
wp_redirect( add_query_arg( array('updated' => 'true'), self_admin_url( 'profile.php' ) ) );

src/wp-includes/user.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2590,3 +2590,113 @@ function _wp_get_current_user() {
25902590

25912591
return $current_user;
25922592
}
2593+
2594+
/**
2595+
* Sends an email when an email address change is requested.
2596+
*
2597+
* @since 3.0.0
2598+
* @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific.
2599+
*
2600+
* @global WP_Error $errors WP_Error object.
2601+
* @global wpdb $wpdb WordPress database object.
2602+
*/
2603+
function send_confirmation_on_profile_email() {
2604+
global $errors, $wpdb;
2605+
2606+
$current_user = wp_get_current_user();
2607+
if ( ! is_object( $errors ) ) {
2608+
$errors = new WP_Error();
2609+
}
2610+
2611+
if ( $current_user->ID != $_POST['user_id'] ) {
2612+
return false;
2613+
}
2614+
2615+
if ( $current_user->user_email != $_POST['email'] ) {
2616+
if ( ! is_email( $_POST['email'] ) ) {
2617+
$errors->add( 'user_email', __( "<strong>ERROR</strong>: The email address isn&#8217;t correct." ), array(
2618+
'form-field' => 'email',
2619+
) );
2620+
2621+
return;
2622+
}
2623+
2624+
if ( $wpdb->get_var( $wpdb->prepare( "SELECT user_email FROM {$wpdb->users} WHERE user_email=%s", $_POST['email'] ) ) ) {
2625+
$errors->add( 'user_email', __( "<strong>ERROR</strong>: The email address is already used." ), array(
2626+
'form-field' => 'email',
2627+
) );
2628+
delete_user_meta( $current_user->ID, '_new_email' );
2629+
2630+
return;
2631+
}
2632+
2633+
$hash = md5( $_POST['email'] . time() . mt_rand() );
2634+
$new_user_email = array(
2635+
'hash' => $hash,
2636+
'newemail' => $_POST['email'],
2637+
);
2638+
update_user_meta( $current_user->ID, '_new_email', $new_user_email );
2639+
2640+
/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
2641+
$email_text = __( 'Howdy ###USERNAME###,
2642+
2643+
You recently requested to have the email address on your account changed.
2644+
2645+
If this is correct, please click on the following link to change it:
2646+
###ADMIN_URL###
2647+
2648+
You can safely ignore and delete this email if you do not want to
2649+
take this action.
2650+
2651+
This email has been sent to ###EMAIL###
2652+
2653+
Regards,
2654+
All at ###SITENAME###
2655+
###SITEURL###' );
2656+
2657+
/**
2658+
* Filters the email text sent when a user changes emails.
2659+
*
2660+
* The following strings have a special meaning and will get replaced dynamically:
2661+
* ###USERNAME### The current user's username.
2662+
* ###ADMIN_URL### The link to click on to confirm the email change.
2663+
* ###EMAIL### The new email.
2664+
* ###SITENAME### The name of the site.
2665+
* ###SITEURL### The URL to the site.
2666+
*
2667+
* @since MU
2668+
* @since 4.9.0 This filter is no longer Multisite specific.
2669+
*
2670+
* @param string $email_text Text in the email.
2671+
* @param string $new_user_email New user email that the current user has changed to.
2672+
*/
2673+
$content = apply_filters( 'new_user_email_content', $email_text, $new_user_email );
2674+
2675+
$content = str_replace( '###USERNAME###', $current_user->user_login, $content );
2676+
$content = str_replace( '###ADMIN_URL###', esc_url( admin_url( 'profile.php?newuseremail=' . $hash ) ), $content );
2677+
$content = str_replace( '###EMAIL###', $_POST['email'], $content );
2678+
$content = str_replace( '###SITENAME###', get_site_option( 'site_name' ), $content );
2679+
$content = str_replace( '###SITEURL###', network_home_url(), $content );
2680+
2681+
wp_mail( $_POST['email'], sprintf( __( '[%s] New Email Address' ), wp_specialchars_decode( get_option( 'blogname' ) ) ), $content );
2682+
2683+
$_POST['email'] = $current_user->user_email;
2684+
}
2685+
}
2686+
2687+
/**
2688+
* Adds an admin notice alerting the user to check for confirmation email
2689+
* after email address change.
2690+
*
2691+
* @since 3.0.0
2692+
* @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific.
2693+
*
2694+
* @global string $pagenow
2695+
*/
2696+
function new_user_email_admin_notice() {
2697+
global $pagenow;
2698+
if ( 'profile.php' === $pagenow && isset( $_GET['updated'] ) && $email = get_user_meta( get_current_user_id(), '_new_email', true ) ) {
2699+
/* translators: %s: New email address */
2700+
echo '<div class="notice notice-info"><p>' . sprintf( __( 'Your email address has not been updated yet. Please check your inbox at %s for a confirmation email.' ), '<code>' . esc_html( $email['newemail'] ) . '</code>' ) . '</p></div>';
2701+
}
2702+
}

tests/phpunit/tests/user.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,4 +1204,72 @@ function test_edit_user_blank_pw() {
12041204
function action_check_passwords_blank_pw( $user_login, &$pass1 ) {
12051205
$pass1 = '';
12061206
}
1207+
1208+
/**
1209+
* @ticket 16470
1210+
*/
1211+
function test_send_confirmation_on_profile_email() {
1212+
reset_phpmailer_instance();
1213+
$was_confirmation_email_sent = false;
1214+
1215+
$user = $this->factory()->user->create_and_get( array(
1216+
'user_email' => 'before@example.com',
1217+
) );
1218+
1219+
$_POST['email'] = 'after@example.com';
1220+
$_POST['user_id'] = $user->ID;
1221+
1222+
wp_set_current_user( $user->ID );
1223+
1224+
do_action( 'personal_options_update' );
1225+
1226+
if ( ! empty( $GLOBALS['phpmailer']->mock_sent ) ) {
1227+
$was_confirmation_email_sent = ( isset( $GLOBALS['phpmailer']->mock_sent[0] ) && 'after@example.com' == $GLOBALS['phpmailer']->mock_sent[0]['to'][0][0] );
1228+
}
1229+
1230+
// A confirmation email is sent.
1231+
$this->assertTrue( $was_confirmation_email_sent );
1232+
1233+
// The new email address gets put into user_meta.
1234+
$new_email_meta = get_user_meta( $user->ID, '_new_email', true );
1235+
$this->assertEquals( 'after@example.com', $new_email_meta['newemail'] );
1236+
1237+
// The email address of the user doesn't change. $_POST['email'] should be the email address pre-update.
1238+
$this->assertEquals( $_POST['email'], $user->user_email );
1239+
}
1240+
1241+
/**
1242+
* @ticket 16470
1243+
*/
1244+
function test_remove_send_confirmation_on_profile_email() {
1245+
remove_action( 'personal_options_update', 'send_confirmation_on_profile_email' );
1246+
1247+
reset_phpmailer_instance();
1248+
$was_confirmation_email_sent = false;
1249+
1250+
$user = $this->factory()->user->create_and_get( array(
1251+
'user_email' => 'before@example.com',
1252+
) );
1253+
1254+
$_POST['email'] = 'after@example.com';
1255+
$_POST['user_id'] = $user->ID;
1256+
1257+
wp_set_current_user( $user->ID );
1258+
1259+
do_action( 'personal_options_update' );
1260+
1261+
if ( ! empty( $GLOBALS['phpmailer']->mock_sent ) ) {
1262+
$was_confirmation_email_sent = ( isset( $GLOBALS['phpmailer']->mock_sent[0] ) && 'after@example.com' == $GLOBALS['phpmailer']->mock_sent[0]['to'][0][0] );
1263+
}
1264+
1265+
// No confirmation email is sent.
1266+
$this->assertFalse( $was_confirmation_email_sent );
1267+
1268+
// No usermeta is created.
1269+
$new_email_meta = get_user_meta( $user->ID, '_new_email', true );
1270+
$this->assertEmpty( $new_email_meta );
1271+
1272+
// $_POST['email'] should be the email address posted from the form.
1273+
$this->assertEquals( $_POST['email'], 'after@example.com' );
1274+
}
12071275
}

0 commit comments

Comments
 (0)