Make WordPress Core

Changeset 50129


Ignore:
Timestamp:
02/01/2021 10:11:46 PM (4 years ago)
Author:
adamsilverstein
Message:

Users: enable admins to send users a reset password link.

Add a feature so Admins can send users a 'password reset' email. This doesn't change the password or force a password change. It only emails the user the password reset link.

The feature appears in several places:

  • A "Send Reset Link" button on user profile screen.
  • A "Send password reset" option in the user list bulk action dropdown.
  • A "Send password reset" quick action when hovering over a username in the user list.

Props Ipstenu, DrewAPicture, eventualo, wonderboymusic, knutsp, ericlewis, afercia, JoshuaWold, johnbillion, paaljoachim, hedgefield.
Fixes #34281.

Location:
trunk/src
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/admin/user-profile.js

    r50001 r50129  
    33 */
    44
    5 /* global ajaxurl, pwsL10n */
     5/* global ajaxurl, pwsL10n, userProfileL10n */
    66(function($) {
    77    var updateLock = false,
     
    9090            }
    9191        });
     92    }
     93
     94    /**
     95     * Handle the password reset button. Sets up an ajax callback to trigger sending
     96     * a password reset email.
     97     */
     98    function bindPasswordRestLink() {
     99        $( '#generate-reset-link' ).on( 'click', function() {
     100            var $this  = $(this),
     101                data = {
     102                    'user_id': userProfileL10n.user_id, // The user to send a reset to.
     103                    'nonce':   userProfileL10n.nonce    // Nonce to validate the action.
     104                };
     105
     106                // Remove any previous error messages.
     107                $this.parent().find( '.notice-error' ).remove();
     108
     109                // Send the reset request.
     110                var resetAction =  wp.ajax.post( 'send-password-reset', data );
     111
     112                // Handle reset success.
     113                resetAction.done( function( response ) {
     114                    addInlineNotice( $this, true, response );
     115                } );
     116
     117                // Handle reset failure.
     118                resetAction.fail( function( response ) {
     119                    addInlineNotice( $this, false, response );
     120                } );
     121
     122        });
     123
     124    }
     125
     126    /**
     127     * Helper function to insert an inline notice of success or failure.
     128     *
     129     * @param {jQuery Object} $this   The button element: the message will be inserted
     130     *                                above this button
     131     * @param {bool}          success Whether the message is a success message.
     132     * @param {string}        message The message to insert.
     133     */
     134    function addInlineNotice( $this, success, message ) {
     135        var resultDiv = $( '<div />' );
     136
     137        // Set up the notice div.
     138        resultDiv.addClass( 'notice inline' );
     139
     140        // Add a class indicating success or failure.
     141        resultDiv.addClass( 'notice-' + ( success ? 'success' : 'error' ) );
     142
     143        // Add the message, wrapping in a p tag, with a fadein to highlight each message.
     144        resultDiv.text( $( $.parseHTML( message ) ).text() ).wrapInner( '<p />');
     145
     146        // Disable the button when the callback has succeeded.
     147        $this.prop( 'disabled', success );
     148
     149        // Remove any previous notices.
     150        $this.siblings( '.notice' ).remove();
     151
     152        // Insert the notice.
     153        $this.before( resultDiv );
    92154    }
    93155
     
    370432
    371433        bindPasswordForm();
     434        bindPasswordRestLink();
    372435    });
    373436
  • trunk/src/wp-admin/admin-ajax.php

    r49154 r50129  
    141141    'health-check-get-sizes',
    142142    'toggle-auto-updates',
     143    'send-password-reset',
    143144);
    144145
  • trunk/src/wp-admin/includes/ajax-actions.php

    r49193 r50129  
    53995399    wp_send_json_success();
    54005400}
     5401
     5402/**
     5403 * Ajax handler sends a password reset link.
     5404 *
     5405 * @since 5.7.0
     5406 */
     5407function wp_ajax_send_password_reset() {
     5408
     5409    // Validate the nonce for this action.
     5410    $user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
     5411    check_ajax_referer( 'reset-password-for-' . $user_id, 'nonce' );
     5412
     5413    // Verify user capabilities.
     5414    if ( ! current_user_can( 'edit_user', $user_id ) ) {
     5415        wp_send_json_error( __( 'Cannot send password reset, permission denied.' ) );
     5416    }
     5417
     5418    // Send the password reset link.
     5419    $user    = get_userdata( $user_id );
     5420    $results = retrieve_password( $user->user_login );
     5421
     5422    if ( true === $results ) {
     5423        wp_send_json_success(
     5424            /* translators: 1: User's display name. */
     5425            sprintf( __( 'A password reset link was emailed to %s.' ), $user->display_name )
     5426        );
     5427    } else {
     5428        wp_send_json_error( $results );
     5429    }
     5430}
  • trunk/src/wp-admin/includes/class-wp-users-list-table.php

    r49944 r50129  
    275275        }
    276276
     277        // Add a password reset link to the bulk actions dropdown.
     278        if ( current_user_can( 'edit_users' ) ) {
     279            $actions['resetpassword'] = __( 'Send password reset' );
     280        }
     281
    277282        return $actions;
    278283    }
     
    468473                    __( 'View' )
    469474                );
     475            }
     476
     477            // Add a link to send the user a reset password link by email.
     478            if ( get_current_user_id() !== $user_object->ID && current_user_can( 'edit_user', $user_object->ID ) ) {
     479                $actions['resetpassword'] = "<a class='resetpassword' href='" . wp_nonce_url( "users.php?action=resetpassword&amp;users=$user_object->ID", 'bulk-users' ) . "'>" . __( 'Send password reset' ) . '</a>';
    470480            }
    471481
  • trunk/src/wp-admin/user-edit.php

    r50006 r50129  
    610610</tr>
    611611<?php endif; ?>
     612        <?php
     613        // Allow admins to send reset password link
     614        if ( ! IS_PROFILE_PAGE ) :
     615            ?>
     616    <tr class="user-sessions-wrap hide-if-no-js">
     617        <th><?php _e( 'Password Reset' ); ?></th>
     618        <td>
     619            <div class="generate-reset-link">
     620                <button type="button" class="button button-secondary" id="generate-reset-link">
     621                    <?php _e( 'Send Reset Link' ); ?>
     622                </button>
     623            </div>
     624            <p class="description">
     625                <?php
     626                /* translators: 1: User's display name. */
     627                printf( __( 'Send %s a link to reset their password. This will not change their password, nor will it force a change.' ), esc_html( $profileuser->display_name ) );
     628                ?>
     629            </p>
     630        </td>
     631    </tr>
     632        <?php endif; ?>
    612633
    613634        <?php
  • trunk/src/wp-admin/users.php

    r49944 r50129  
    203203                'delete_count' => $delete_count,
    204204                'update'       => $update,
     205            ),
     206            $redirect
     207        );
     208        wp_redirect( $redirect );
     209        exit;
     210
     211    case 'resetpassword':
     212        check_admin_referer( 'bulk-users' );
     213        if ( ! current_user_can( 'edit_users' ) ) {
     214            $errors = new WP_Error( 'edit_users', __( 'You can&#8217;t edit users.' ) );
     215        }
     216        if ( empty( $_REQUEST['users'] ) ) {
     217            wp_redirect( $redirect );
     218            exit();
     219        }
     220        $userids = array_map( 'intval', (array) $_REQUEST['users'] );
     221
     222        $reset_count = 0;
     223
     224        foreach ( $userids as $id ) {
     225            if ( ! current_user_can( 'edit_user', $id ) ) {
     226                wp_die( __( 'You can&#8217;t edit that user.' ) );
     227            }
     228
     229            if ( $id === $current_user->ID ) {
     230                $update = 'err_admin_reset';
     231                continue;
     232            }
     233
     234            // Send the password reset link.
     235            $user = get_userdata( $id );
     236            if ( retrieve_password( $user->user_login ) ) {
     237                ++$reset_count;
     238            }
     239        }
     240
     241        $redirect = add_query_arg(
     242            array(
     243                'reset_count' => $reset_count,
     244                'update'      => 'resetpassword',
    205245            ),
    206246            $redirect
     
    507547                    $messages[] = '<div id="message" class="updated notice is-dismissible"><p>' . $message . '</p></div>';
    508548                    break;
     549                case 'resetpassword':
     550                    $reset_count = isset( $_GET['reset_count'] ) ? (int) $_GET['reset_count'] : 0;
     551                    if ( 1 === $reset_count ) {
     552                        $message = __( 'Password reset link sent.' );
     553                    } else {
     554                        /* translators: %s: Number of users. */
     555                        $message = sprintf( __( 'Password reset links sent to %s users.' ), $reset_count );
     556                    }
     557                    $messages[] = '<div id="message" class="updated notice is-dismissible"><p>' . $message . '</p></div>';
     558                    break;
    509559                case 'promote':
    510560                    $messages[] = '<div id="message" class="updated notice is-dismissible"><p>' . __( 'Changed roles.' ) . '</p></div>';
  • trunk/src/wp-includes/functions.php

    r50037 r50129  
    77827782    return abs( (float) $expected - (float) $actual ) <= $precision;
    77837783}
     7784
     7785/**
     7786 * Handles sending a password retrieval email to a user.
     7787 *
     7788 * @since 2.5.0
     7789 * @since 5.7.0 Added `$user_login` parameter.
     7790 *
     7791 * Note: prior to 5.7.0 this function was in wp_login.php.
     7792 *
     7793 * @global wpdb         $wpdb       WordPress database abstraction object.
     7794 * @global PasswordHash $wp_hasher  Portable PHP password hashing framework.
     7795 *
     7796 * @param  string       $user_login Optional user_login, default null. Uses
     7797 *                                  `$_POST['user_login']` if `$user_login` not set.
     7798 * @return true|WP_Error True when finished, WP_Error object on error.
     7799 */
     7800function retrieve_password( $user_login = null ) {
     7801    $errors    = new WP_Error();
     7802    $user_data = false;
     7803
     7804    // Use the passed $user_login if available, otherwise use $_POST['user_login'].
     7805    if ( ! $user_login && ! empty( $_POST['user_login'] ) ) {
     7806        $user_login = $_POST['user_login'];
     7807    }
     7808
     7809    if ( empty( $user_login ) ) {
     7810        $errors->add( 'empty_username', __( '<strong>Error</strong>: Please enter a username or email address.' ) );
     7811    } elseif ( strpos( $user_login, '@' ) ) {
     7812        $user_data = get_user_by( 'email', trim( wp_unslash( $user_login ) ) );
     7813        if ( empty( $user_data ) ) {
     7814            $errors->add( 'invalid_email', __( '<strong>Error</strong>: There is no account with that username or email address.' ) );
     7815        }
     7816    } else {
     7817        $user_data = get_user_by( 'login', trim( wp_unslash( $user_login ) ) );
     7818    }
     7819
     7820    /**
     7821     * Filters the user data during a password reset request.
     7822     *
     7823     * Allows, for example, custom validation using data other than username or email address.
     7824     *
     7825     * @since 5.7.0
     7826     *
     7827     * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
     7828     * @param WP_Error      $errors    A WP_Error object containing any errors generated
     7829     *                                 by using invalid credentials.
     7830     */
     7831    $user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors );
     7832
     7833    /**
     7834     * Fires before errors are returned from a password reset request.
     7835     *
     7836     * @since 2.1.0
     7837     * @since 4.4.0 Added the `$errors` parameter.
     7838     * @since 5.4.0 Added the `$user_data` parameter.
     7839     *
     7840     * @param WP_Error      $errors    A WP_Error object containing any errors generated
     7841     *                                 by using invalid credentials.
     7842     * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
     7843     */
     7844    do_action( 'lostpassword_post', $errors, $user_data );
     7845
     7846    /**
     7847     * Filters the errors encountered on a password reset request.
     7848     *
     7849     * The filtered WP_Error object may, for example, contain errors for an invalid
     7850     * username or email address. A WP_Error object should always be returned,
     7851     * but may or may not contain errors.
     7852     *
     7853     * If any errors are present in $errors, this will abort the password reset request.
     7854     *
     7855     * @since 5.5.0
     7856     *
     7857     * @param WP_Error      $errors    A WP_Error object containing any errors generated
     7858     *                                 by using invalid credentials.
     7859     * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
     7860     */
     7861    $errors = apply_filters( 'lostpassword_errors', $errors, $user_data );
     7862
     7863    if ( $errors->has_errors() ) {
     7864        return $errors;
     7865    }
     7866
     7867    if ( ! $user_data ) {
     7868        $errors->add( 'invalidcombo', __( '<strong>Error</strong>: There is no account with that username or email address.' ) );
     7869        return $errors;
     7870    }
     7871
     7872    // Redefining user_login ensures we return the right case in the email.
     7873    $user_login = $user_data->user_login;
     7874    $user_email = $user_data->user_email;
     7875    $key        = get_password_reset_key( $user_data );
     7876
     7877    if ( is_wp_error( $key ) ) {
     7878        return $key;
     7879    }
     7880
     7881    if ( is_multisite() ) {
     7882        $site_name = get_network()->site_name;
     7883    } else {
     7884        /*
     7885         * The blogname option is escaped with esc_html on the way into the database
     7886         * in sanitize_option. We want to reverse this for the plain text arena of emails.
     7887         */
     7888        $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
     7889    }
     7890
     7891    $message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n";
     7892    /* translators: %s: Site name. */
     7893    $message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n";
     7894    /* translators: %s: User login. */
     7895    $message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n";
     7896    $message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n";
     7897    $message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n";
     7898    $message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . "\r\n\r\n";
     7899
     7900    $requester_ip = $_SERVER['REMOTE_ADDR'];
     7901    if ( $requester_ip ) {
     7902        $message .= sprintf(
     7903            /* translators: %s: IP address of password reset requester. */
     7904            __( 'This password reset request originated from the IP address %s.' ),
     7905            $requester_ip
     7906        ) . "\r\n";
     7907    }
     7908
     7909    /* translators: Password reset notification email subject. %s: Site title. */
     7910    $title = sprintf( __( '[%s] Password Reset' ), $site_name );
     7911
     7912    /**
     7913     * Filters the subject of the password reset email.
     7914     *
     7915     * @since 2.8.0
     7916     * @since 4.4.0 Added the `$user_login` and `$user_data` parameters.
     7917     *
     7918     * @param string  $title      Email subject.
     7919     * @param string  $user_login The username for the user.
     7920     * @param WP_User $user_data  WP_User object.
     7921     */
     7922    $title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data );
     7923
     7924    /**
     7925     * Filters the message body of the password reset mail.
     7926     *
     7927     * If the filtered message is empty, the password reset email will not be sent.
     7928     *
     7929     * @since 2.8.0
     7930     * @since 4.1.0 Added `$user_login` and `$user_data` parameters.
     7931     *
     7932     * @param string  $message    Email message.
     7933     * @param string  $key        The activation key.
     7934     * @param string  $user_login The username for the user.
     7935     * @param WP_User $user_data  WP_User object.
     7936     */
     7937    $message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data );
     7938
     7939    if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) {
     7940        $errors->add(
     7941            'retrieve_password_email_failure',
     7942            sprintf(
     7943                /* translators: %s: Documentation URL. */
     7944                __( '<strong>Error</strong>: The email could not be sent. Your site may not be correctly configured to send emails. <a href="%s">Get support for resetting your password</a>.' ),
     7945                esc_url( __( 'https://wordpress.org/support/article/resetting-your-password/' ) )
     7946            )
     7947        );
     7948        return $errors;
     7949    }
     7950
     7951    return true;
     7952}
  • trunk/src/wp-includes/script-loader.php

    r50048 r50129  
    10791079    $scripts->add( 'user-profile', "/wp-admin/js/user-profile$suffix.js", array( 'jquery', 'password-strength-meter', 'wp-util' ), false, 1 );
    10801080    $scripts->set_translations( 'user-profile' );
     1081    $user_id = isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : 0;
     1082    did_action( 'init' ) && $scripts->localize(
     1083        'user-profile',
     1084        'userProfileL10n',
     1085        array(
     1086            'user_id'  => $user_id,
     1087            'nonce'    => wp_create_nonce( 'reset-password-for-' . $user_id ),
     1088        )
     1089    );
    10811090
    10821091    $scripts->add( 'language-chooser', "/wp-admin/js/language-chooser$suffix.js", array( 'jquery' ), false, 1 );
  • trunk/src/wp-login.php

    r50117 r50129  
    359359    <meta name="viewport" content="width=device-width" />
    360360    <?php
    361 }
    362 
    363 /**
    364  * Handles sending a password retrieval email to a user.
    365  *
    366  * @since 2.5.0
    367  *
    368  * @return true|WP_Error True when finished, WP_Error object on error.
    369  */
    370 function retrieve_password() {
    371     $errors    = new WP_Error();
    372     $user_data = false;
    373 
    374     if ( empty( $_POST['user_login'] ) || ! is_string( $_POST['user_login'] ) ) {
    375         $errors->add( 'empty_username', __( '<strong>Error</strong>: Please enter a username or email address.' ) );
    376     } elseif ( strpos( $_POST['user_login'], '@' ) ) {
    377         $user_data = get_user_by( 'email', trim( wp_unslash( $_POST['user_login'] ) ) );
    378         if ( empty( $user_data ) ) {
    379             $errors->add( 'invalid_email', __( '<strong>Error</strong>: There is no account with that username or email address.' ) );
    380         }
    381     } else {
    382         $login     = trim( wp_unslash( $_POST['user_login'] ) );
    383         $user_data = get_user_by( 'login', $login );
    384     }
    385 
    386     /**
    387      * Filters the user data during a password reset request.
    388      *
    389      * Allows, for example, custom validation using data other than username or email address.
    390      *
    391      * @since 5.7.0
    392      *
    393      * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
    394      * @param WP_Error      $errors    A WP_Error object containing any errors generated
    395      *                                 by using invalid credentials.
    396      */
    397     $user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors );
    398 
    399     /**
    400      * Fires before errors are returned from a password reset request.
    401      *
    402      * @since 2.1.0
    403      * @since 4.4.0 Added the `$errors` parameter.
    404      * @since 5.4.0 Added the `$user_data` parameter.
    405      *
    406      * @param WP_Error      $errors    A WP_Error object containing any errors generated
    407      *                                 by using invalid credentials.
    408      * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
    409      */
    410     do_action( 'lostpassword_post', $errors, $user_data );
    411 
    412     /**
    413      * Filters the errors encountered on a password reset request.
    414      *
    415      * The filtered WP_Error object may, for example, contain errors for an invalid
    416      * username or email address. A WP_Error object should always be returned,
    417      * but may or may not contain errors.
    418      *
    419      * If any errors are present in $errors, this will abort the password reset request.
    420      *
    421      * @since 5.5.0
    422      *
    423      * @param WP_Error      $errors    A WP_Error object containing any errors generated
    424      *                                 by using invalid credentials.
    425      * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
    426      */
    427     $errors = apply_filters( 'lostpassword_errors', $errors, $user_data );
    428 
    429     if ( $errors->has_errors() ) {
    430         return $errors;
    431     }
    432 
    433     if ( ! $user_data ) {
    434         $errors->add( 'invalidcombo', __( '<strong>Error</strong>: There is no account with that username or email address.' ) );
    435         return $errors;
    436     }
    437 
    438     // Redefining user_login ensures we return the right case in the email.
    439     $user_login = $user_data->user_login;
    440     $user_email = $user_data->user_email;
    441     $key        = get_password_reset_key( $user_data );
    442 
    443     if ( is_wp_error( $key ) ) {
    444         return $key;
    445     }
    446 
    447     if ( is_multisite() ) {
    448         $site_name = get_network()->site_name;
    449     } else {
    450         /*
    451          * The blogname option is escaped with esc_html on the way into the database
    452          * in sanitize_option. We want to reverse this for the plain text arena of emails.
    453          */
    454         $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
    455     }
    456 
    457     $message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n";
    458     /* translators: %s: Site name. */
    459     $message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n";
    460     /* translators: %s: User login. */
    461     $message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n";
    462     $message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n";
    463     $message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n";
    464     $message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . "\r\n\r\n";
    465 
    466     $requester_ip = $_SERVER['REMOTE_ADDR'];
    467     if ( $requester_ip ) {
    468         $message .= sprintf(
    469             /* translators: %s: IP address of password reset requester. */
    470             __( 'This password reset request originated from the IP address %s.' ),
    471             $requester_ip
    472         ) . "\r\n";
    473     }
    474 
    475     /* translators: Password reset notification email subject. %s: Site title. */
    476     $title = sprintf( __( '[%s] Password Reset' ), $site_name );
    477 
    478     /**
    479      * Filters the subject of the password reset email.
    480      *
    481      * @since 2.8.0
    482      * @since 4.4.0 Added the `$user_login` and `$user_data` parameters.
    483      *
    484      * @param string  $title      Email subject.
    485      * @param string  $user_login The username for the user.
    486      * @param WP_User $user_data  WP_User object.
    487      */
    488     $title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data );
    489 
    490     /**
    491      * Filters the message body of the password reset mail.
    492      *
    493      * If the filtered message is empty, the password reset email will not be sent.
    494      *
    495      * @since 2.8.0
    496      * @since 4.1.0 Added `$user_login` and `$user_data` parameters.
    497      *
    498      * @param string  $message    Email message.
    499      * @param string  $key        The activation key.
    500      * @param string  $user_login The username for the user.
    501      * @param WP_User $user_data  WP_User object.
    502      */
    503     $message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data );
    504 
    505     if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) {
    506         $errors->add(
    507             'retrieve_password_email_failure',
    508             sprintf(
    509                 /* translators: %s: Documentation URL. */
    510                 __( '<strong>Error</strong>: The email could not be sent. Your site may not be correctly configured to send emails. <a href="%s">Get support for resetting your password</a>.' ),
    511                 esc_url( __( 'https://wordpress.org/support/article/resetting-your-password/' ) )
    512             )
    513         );
    514         return $errors;
    515     }
    516 
    517     return true;
    518361}
    519362
Note: See TracChangeset for help on using the changeset viewer.