diff --git a/wp-admin/admin-ajax.php b/wp-admin/admin-ajax.php index 144facf7fe..30e9a414b5 100644 --- a/wp-admin/admin-ajax.php +++ b/wp-admin/admin-ajax.php @@ -140,6 +140,7 @@ $core_actions_post = array( 'health-check-loopback-requests', 'health-check-get-sizes', 'toggle-auto-updates', + 'send-password-reset', ); // Deprecated. diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php index 6ff2d2d9fd..af8f053fe0 100644 --- a/wp-admin/includes/ajax-actions.php +++ b/wp-admin/includes/ajax-actions.php @@ -5398,3 +5398,33 @@ function wp_ajax_toggle_auto_updates() { wp_send_json_success(); } + +/** + * Ajax handler sends a password reset link. + * + * @since 5.7.0 + */ +function wp_ajax_send_password_reset() { + + // Validate the nonce for this action. + $user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0; + check_ajax_referer( 'reset-password-for-' . $user_id, 'nonce' ); + + // Verify user capabilities. + if ( ! current_user_can( 'edit_user', $user_id ) ) { + wp_send_json_error( __( 'Cannot send password reset, permission denied.' ) ); + } + + // Send the password reset link. + $user = get_userdata( $user_id ); + $results = retrieve_password( $user->user_login ); + + if ( true === $results ) { + wp_send_json_success( + /* translators: 1: User's display name. */ + sprintf( __( 'A password reset link was emailed to %s.' ), $user->display_name ) + ); + } else { + wp_send_json_error( $results ); + } +} diff --git a/wp-admin/includes/class-wp-users-list-table.php b/wp-admin/includes/class-wp-users-list-table.php index 7e1e79f500..bf73df9e99 100644 --- a/wp-admin/includes/class-wp-users-list-table.php +++ b/wp-admin/includes/class-wp-users-list-table.php @@ -274,6 +274,11 @@ class WP_Users_List_Table extends WP_List_Table { } } + // Add a password reset link to the bulk actions dropdown. + if ( current_user_can( 'edit_users' ) ) { + $actions['resetpassword'] = __( 'Send password reset' ); + } + return $actions; } @@ -469,6 +474,11 @@ class WP_Users_List_Table extends WP_List_Table { ); } + // Add a link to send the user a reset password link by email. + if ( get_current_user_id() !== $user_object->ID && current_user_can( 'edit_user', $user_object->ID ) ) { + $actions['resetpassword'] = "" . __( 'Send password reset' ) . ''; + } + /** * Filters the action links displayed under each user in the Users list table. * diff --git a/wp-admin/js/user-profile.js b/wp-admin/js/user-profile.js index df17620f48..c8f8d03261 100644 --- a/wp-admin/js/user-profile.js +++ b/wp-admin/js/user-profile.js @@ -2,7 +2,7 @@ * @output wp-admin/js/user-profile.js */ -/* global ajaxurl, pwsL10n */ +/* global ajaxurl, pwsL10n, userProfileL10n */ (function($) { var updateLock = false, __ = wp.i18n.__, @@ -91,6 +91,68 @@ }); } + /** + * Handle the password reset button. Sets up an ajax callback to trigger sending + * a password reset email. + */ + function bindPasswordRestLink() { + $( '#generate-reset-link' ).on( 'click', function() { + var $this = $(this), + data = { + 'user_id': userProfileL10n.user_id, // The user to send a reset to. + 'nonce': userProfileL10n.nonce // Nonce to validate the action. + }; + + // Remove any previous error messages. + $this.parent().find( '.notice-error' ).remove(); + + // Send the reset request. + var resetAction = wp.ajax.post( 'send-password-reset', data ); + + // Handle reset success. + resetAction.done( function( response ) { + addInlineNotice( $this, true, response ); + } ); + + // Handle reset failure. + resetAction.fail( function( response ) { + addInlineNotice( $this, false, response ); + } ); + + }); + + } + + /** + * Helper function to insert an inline notice of success or failure. + * + * @param {jQuery Object} $this The button element: the message will be inserted + * above this button + * @param {bool} success Whether the message is a success message. + * @param {string} message The message to insert. + */ + function addInlineNotice( $this, success, message ) { + var resultDiv = $( '
' ); + + // Set up the notice div. + resultDiv.addClass( 'notice inline' ); + + // Add a class indicating success or failure. + resultDiv.addClass( 'notice-' + ( success ? 'success' : 'error' ) ); + + // Add the message, wrapping in a p tag, with a fadein to highlight each message. + resultDiv.text( $( $.parseHTML( message ) ).text() ).wrapInner( '

'); + + // Disable the button when the callback has succeeded. + $this.prop( 'disabled', success ); + + // Remove any previous notices. + $this.siblings( '.notice' ).remove(); + + // Insert the notice. + $this.before( resultDiv ); + } + function bindPasswordForm() { var $generateButton, $cancelButton; @@ -369,6 +431,7 @@ }); bindPasswordForm(); + bindPasswordRestLink(); }); $( '#destroy-sessions' ).on( 'click', function( e ) { diff --git a/wp-admin/js/user-profile.min.js b/wp-admin/js/user-profile.min.js index 2556fadbb7..a43f6ab9e1 100644 --- a/wp-admin/js/user-profile.min.js +++ b/wp-admin/js/user-profile.min.js @@ -1,2 +1,2 @@ /*! This file is auto-generated */ -!function(r){var e,a,t,n,i,o,d,l,p,c,u=!1,h=wp.i18n.__;function w(){"function"==typeof zxcvbn?(!a.val()||c.hasClass("is-open")?(a.val(a.data("pw")),a.trigger("pwupdate")):v(),b(),1!==parseInt(o.data("start-masked"),10)?a.attr("type","text"):o.trigger("click"),r("#pw-weak-text-label").text(h("Confirm use of weak password"))):setTimeout(w,50)}function m(s){o.attr({"aria-label":h(s?"Show password":"Hide password")}).find(".text").text(h(s?"Show":"Hide")).end().find(".dashicons").removeClass(s?"dashicons-hidden":"dashicons-visibility").addClass(s?"dashicons-visibility":"dashicons-hidden")}function f(){var s;e=r(".user-pass1-wrap, .user-pass-wrap"),r(".user-pass2-wrap").hide(),l=r("#submit, #wp-submit").on("click",function(){u=!1}),d=l.add(" #createusersub"),n=r(".pw-weak"),(i=n.find(".pw-checkbox")).on("change",function(){d.prop("disabled",!i.prop("checked"))}),(a=r("#pass1")).length?(p=a.val(),1===parseInt(a.data("reveal"),10)&&w(),a.on("input pwupdate",function(){a.val()!==p&&(p=a.val(),a.removeClass("short bad good strong"),b())})):a=r("#user_pass"),t=r("#pass2").on("input",function(){0]*>/gi,""),t[s].length&&-1===r.inArray(e,a)&&(a.push(e),r("

'+s.message+"

")}).fail(function(s){e.siblings(".notice").remove(),e.before('

'+s.message+"

")}),s.preventDefault()}),window.generatePassword=w,r(window).on("beforeunload",function(){if(!0===u)return h("Your new password has not been saved.")})}(jQuery); \ No newline at end of file +!function(r){var e,a,n,t,i,o,d,l,p,c,u=!1,h=wp.i18n.__;function f(){"function"==typeof zxcvbn?(!a.val()||c.hasClass("is-open")?(a.val(a.data("pw")),a.trigger("pwupdate")):b(),g(),1!==parseInt(o.data("start-masked"),10)?a.attr("type","text"):o.trigger("click"),r("#pw-weak-text-label").text(h("Confirm use of weak password"))):setTimeout(f,50)}function w(s){o.attr({"aria-label":h(s?"Show password":"Hide password")}).find(".text").text(h(s?"Show":"Hide")).end().find(".dashicons").removeClass(s?"dashicons-hidden":"dashicons-visibility").addClass(s?"dashicons-visibility":"dashicons-hidden")}function m(s,e,a){var n=r("
");n.addClass("notice inline"),n.addClass("notice-"+(e?"success":"error")),n.text(r(r.parseHTML(a)).text()).wrapInner("

"),s.prop("disabled",e),s.siblings(".notice").remove(),s.before(n)}function v(){var s;e=r(".user-pass1-wrap, .user-pass-wrap"),r(".user-pass2-wrap").hide(),l=r("#submit, #wp-submit").on("click",function(){u=!1}),d=l.add(" #createusersub"),t=r(".pw-weak"),(i=t.find(".pw-checkbox")).on("change",function(){d.prop("disabled",!i.prop("checked"))}),(a=r("#pass1")).length?(p=a.val(),1===parseInt(a.data("reveal"),10)&&f(),a.on("input pwupdate",function(){a.val()!==p&&(p=a.val(),a.removeClass("short bad good strong"),g())})):a=r("#user_pass"),n=r("#pass2").on("input",function(){0]*>/gi,""),n[s].length&&-1===r.inArray(e,a)&&(a.push(e),r("

'+s.message+"

")}).fail(function(s){e.siblings(".notice").remove(),e.before('

'+s.message+"

")}),s.preventDefault()}),window.generatePassword=f,r(window).on("beforeunload",function(){if(!0===u)return h("Your new password has not been saved.")})}(jQuery); \ No newline at end of file diff --git a/wp-admin/user-edit.php b/wp-admin/user-edit.php index b09f14fac5..4b46af00e8 100644 --- a/wp-admin/user-edit.php +++ b/wp-admin/user-edit.php @@ -609,6 +609,27 @@ endif; + + + + + +

+ display_name ) ); + ?> +

+ + + current_action() ) { wp_redirect( $redirect ); exit; + case 'resetpassword': + check_admin_referer( 'bulk-users' ); + if ( ! current_user_can( 'edit_users' ) ) { + $errors = new WP_Error( 'edit_users', __( 'You can’t edit users.' ) ); + } + if ( empty( $_REQUEST['users'] ) ) { + wp_redirect( $redirect ); + exit(); + } + $userids = array_map( 'intval', (array) $_REQUEST['users'] ); + + $reset_count = 0; + + foreach ( $userids as $id ) { + if ( ! current_user_can( 'edit_user', $id ) ) { + wp_die( __( 'You can’t edit that user.' ) ); + } + + if ( $id === $current_user->ID ) { + $update = 'err_admin_reset'; + continue; + } + + // Send the password reset link. + $user = get_userdata( $id ); + if ( retrieve_password( $user->user_login ) ) { + ++$reset_count; + } + } + + $redirect = add_query_arg( + array( + 'reset_count' => $reset_count, + 'update' => 'resetpassword', + ), + $redirect + ); + wp_redirect( $redirect ); + exit; + case 'delete': if ( is_multisite() ) { wp_die( __( 'User deletion is not allowed from this screen.' ), 400 ); @@ -504,6 +544,16 @@ switch ( $wp_list_table->current_action() ) { ); } + $messages[] = '

' . $message . '

'; + break; + case 'resetpassword': + $reset_count = isset( $_GET['reset_count'] ) ? (int) $_GET['reset_count'] : 0; + if ( 1 === $reset_count ) { + $message = __( 'Password reset link sent.' ); + } else { + /* translators: %s: Number of users. */ + $message = sprintf( __( 'Password reset links sent to %s users.' ), $reset_count ); + } $messages[] = '

' . $message . '

'; break; case 'promote': diff --git a/wp-includes/functions.php b/wp-includes/functions.php index 536f195b14..8a5826d865 100644 --- a/wp-includes/functions.php +++ b/wp-includes/functions.php @@ -7781,3 +7781,172 @@ function is_php_version_compatible( $required ) { function wp_fuzzy_number_match( $expected, $actual, $precision = 1 ) { return abs( (float) $expected - (float) $actual ) <= $precision; } + +/** + * Handles sending a password retrieval email to a user. + * + * @since 2.5.0 + * @since 5.7.0 Added `$user_login` parameter. + * + * Note: prior to 5.7.0 this function was in wp_login.php. + * + * @global wpdb $wpdb WordPress database abstraction object. + * @global PasswordHash $wp_hasher Portable PHP password hashing framework. + * + * @param string $user_login Optional user_login, default null. Uses + * `$_POST['user_login']` if `$user_login` not set. + * @return true|WP_Error True when finished, WP_Error object on error. + */ +function retrieve_password( $user_login = null ) { + $errors = new WP_Error(); + $user_data = false; + + // Use the passed $user_login if available, otherwise use $_POST['user_login']. + if ( ! $user_login && ! empty( $_POST['user_login'] ) ) { + $user_login = $_POST['user_login']; + } + + if ( empty( $user_login ) ) { + $errors->add( 'empty_username', __( 'Error: Please enter a username or email address.' ) ); + } elseif ( strpos( $user_login, '@' ) ) { + $user_data = get_user_by( 'email', trim( wp_unslash( $user_login ) ) ); + if ( empty( $user_data ) ) { + $errors->add( 'invalid_email', __( 'Error: There is no account with that username or email address.' ) ); + } + } else { + $user_data = get_user_by( 'login', trim( wp_unslash( $user_login ) ) ); + } + + /** + * Filters the user data during a password reset request. + * + * Allows, for example, custom validation using data other than username or email address. + * + * @since 5.7.0 + * + * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. + * @param WP_Error $errors A WP_Error object containing any errors generated + * by using invalid credentials. + */ + $user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors ); + + /** + * Fires before errors are returned from a password reset request. + * + * @since 2.1.0 + * @since 4.4.0 Added the `$errors` parameter. + * @since 5.4.0 Added the `$user_data` parameter. + * + * @param WP_Error $errors A WP_Error object containing any errors generated + * by using invalid credentials. + * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. + */ + do_action( 'lostpassword_post', $errors, $user_data ); + + /** + * Filters the errors encountered on a password reset request. + * + * The filtered WP_Error object may, for example, contain errors for an invalid + * username or email address. A WP_Error object should always be returned, + * but may or may not contain errors. + * + * If any errors are present in $errors, this will abort the password reset request. + * + * @since 5.5.0 + * + * @param WP_Error $errors A WP_Error object containing any errors generated + * by using invalid credentials. + * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. + */ + $errors = apply_filters( 'lostpassword_errors', $errors, $user_data ); + + if ( $errors->has_errors() ) { + return $errors; + } + + if ( ! $user_data ) { + $errors->add( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); + return $errors; + } + + // Redefining user_login ensures we return the right case in the email. + $user_login = $user_data->user_login; + $user_email = $user_data->user_email; + $key = get_password_reset_key( $user_data ); + + if ( is_wp_error( $key ) ) { + return $key; + } + + if ( is_multisite() ) { + $site_name = get_network()->site_name; + } else { + /* + * The blogname option is escaped with esc_html on the way into the database + * in sanitize_option. We want to reverse this for the plain text arena of emails. + */ + $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + } + + $message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n"; + /* translators: %s: Site name. */ + $message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n"; + /* translators: %s: User login. */ + $message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n"; + $message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n"; + $message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n"; + $message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . "\r\n\r\n"; + + $requester_ip = $_SERVER['REMOTE_ADDR']; + if ( $requester_ip ) { + $message .= sprintf( + /* translators: %s: IP address of password reset requester. */ + __( 'This password reset request originated from the IP address %s.' ), + $requester_ip + ) . "\r\n"; + } + + /* translators: Password reset notification email subject. %s: Site title. */ + $title = sprintf( __( '[%s] Password Reset' ), $site_name ); + + /** + * Filters the subject of the password reset email. + * + * @since 2.8.0 + * @since 4.4.0 Added the `$user_login` and `$user_data` parameters. + * + * @param string $title Email subject. + * @param string $user_login The username for the user. + * @param WP_User $user_data WP_User object. + */ + $title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data ); + + /** + * Filters the message body of the password reset mail. + * + * If the filtered message is empty, the password reset email will not be sent. + * + * @since 2.8.0 + * @since 4.1.0 Added `$user_login` and `$user_data` parameters. + * + * @param string $message Email message. + * @param string $key The activation key. + * @param string $user_login The username for the user. + * @param WP_User $user_data WP_User object. + */ + $message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data ); + + if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) { + $errors->add( + 'retrieve_password_email_failure', + sprintf( + /* translators: %s: Documentation URL. */ + __( 'Error: The email could not be sent. Your site may not be correctly configured to send emails. Get support for resetting your password.' ), + esc_url( __( 'https://wordpress.org/support/article/resetting-your-password/' ) ) + ) + ); + return $errors; + } + + return true; +} diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php index 59fef34afa..9d9fe39825 100644 --- a/wp-includes/script-loader.php +++ b/wp-includes/script-loader.php @@ -1078,6 +1078,15 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'user-profile', "/wp-admin/js/user-profile$suffix.js", array( 'jquery', 'password-strength-meter', 'wp-util' ), false, 1 ); $scripts->set_translations( 'user-profile' ); + $user_id = isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : 0; + did_action( 'init' ) && $scripts->localize( + 'user-profile', + 'userProfileL10n', + array( + 'user_id' => $user_id, + 'nonce' => wp_create_nonce( 'reset-password-for-' . $user_id ), + ) + ); $scripts->add( 'language-chooser', "/wp-admin/js/language-chooser$suffix.js", array( 'jquery' ), false, 1 ); diff --git a/wp-includes/version.php b/wp-includes/version.php index 3a95de8f3b..f3652f0670 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @global string $wp_version */ -$wp_version = '5.7-alpha-50128'; +$wp_version = '5.7-alpha-50129'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. diff --git a/wp-login.php b/wp-login.php index 75c65865d9..128e3b254c 100644 --- a/wp-login.php +++ b/wp-login.php @@ -360,163 +360,6 @@ function wp_login_viewport_meta() { add( 'empty_username', __( 'Error: Please enter a username or email address.' ) ); - } elseif ( strpos( $_POST['user_login'], '@' ) ) { - $user_data = get_user_by( 'email', trim( wp_unslash( $_POST['user_login'] ) ) ); - if ( empty( $user_data ) ) { - $errors->add( 'invalid_email', __( 'Error: There is no account with that username or email address.' ) ); - } - } else { - $login = trim( wp_unslash( $_POST['user_login'] ) ); - $user_data = get_user_by( 'login', $login ); - } - - /** - * Filters the user data during a password reset request. - * - * Allows, for example, custom validation using data other than username or email address. - * - * @since 5.7.0 - * - * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. - * @param WP_Error $errors A WP_Error object containing any errors generated - * by using invalid credentials. - */ - $user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors ); - - /** - * Fires before errors are returned from a password reset request. - * - * @since 2.1.0 - * @since 4.4.0 Added the `$errors` parameter. - * @since 5.4.0 Added the `$user_data` parameter. - * - * @param WP_Error $errors A WP_Error object containing any errors generated - * by using invalid credentials. - * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. - */ - do_action( 'lostpassword_post', $errors, $user_data ); - - /** - * Filters the errors encountered on a password reset request. - * - * The filtered WP_Error object may, for example, contain errors for an invalid - * username or email address. A WP_Error object should always be returned, - * but may or may not contain errors. - * - * If any errors are present in $errors, this will abort the password reset request. - * - * @since 5.5.0 - * - * @param WP_Error $errors A WP_Error object containing any errors generated - * by using invalid credentials. - * @param WP_User|false $user_data WP_User object if found, false if the user does not exist. - */ - $errors = apply_filters( 'lostpassword_errors', $errors, $user_data ); - - if ( $errors->has_errors() ) { - return $errors; - } - - if ( ! $user_data ) { - $errors->add( 'invalidcombo', __( 'Error: There is no account with that username or email address.' ) ); - return $errors; - } - - // Redefining user_login ensures we return the right case in the email. - $user_login = $user_data->user_login; - $user_email = $user_data->user_email; - $key = get_password_reset_key( $user_data ); - - if ( is_wp_error( $key ) ) { - return $key; - } - - if ( is_multisite() ) { - $site_name = get_network()->site_name; - } else { - /* - * The blogname option is escaped with esc_html on the way into the database - * in sanitize_option. We want to reverse this for the plain text arena of emails. - */ - $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); - } - - $message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n"; - /* translators: %s: Site name. */ - $message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n"; - /* translators: %s: User login. */ - $message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n"; - $message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n"; - $message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n"; - $message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . "\r\n\r\n"; - - $requester_ip = $_SERVER['REMOTE_ADDR']; - if ( $requester_ip ) { - $message .= sprintf( - /* translators: %s: IP address of password reset requester. */ - __( 'This password reset request originated from the IP address %s.' ), - $requester_ip - ) . "\r\n"; - } - - /* translators: Password reset notification email subject. %s: Site title. */ - $title = sprintf( __( '[%s] Password Reset' ), $site_name ); - - /** - * Filters the subject of the password reset email. - * - * @since 2.8.0 - * @since 4.4.0 Added the `$user_login` and `$user_data` parameters. - * - * @param string $title Email subject. - * @param string $user_login The username for the user. - * @param WP_User $user_data WP_User object. - */ - $title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data ); - - /** - * Filters the message body of the password reset mail. - * - * If the filtered message is empty, the password reset email will not be sent. - * - * @since 2.8.0 - * @since 4.1.0 Added `$user_login` and `$user_data` parameters. - * - * @param string $message Email message. - * @param string $key The activation key. - * @param string $user_login The username for the user. - * @param WP_User $user_data WP_User object. - */ - $message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data ); - - if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) { - $errors->add( - 'retrieve_password_email_failure', - sprintf( - /* translators: %s: Documentation URL. */ - __( 'Error: The email could not be sent. Your site may not be correctly configured to send emails. Get support for resetting your password.' ), - esc_url( __( 'https://wordpress.org/support/article/resetting-your-password/' ) ) - ) - ); - return $errors; - } - - return true; -} - // // Main. //