Skip to content

Commit 193a5da

Browse files
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. git-svn-id: https://develop.svn.wordpress.org/trunk@50129 602fd350-edb4-49c9-b593-d223f7449a82
1 parent c2bc4df commit 193a5da

9 files changed

Lines changed: 354 additions & 158 deletions

File tree

src/js/_enqueues/admin/user-profile.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @output wp-admin/js/user-profile.js
33
*/
44

5-
/* global ajaxurl, pwsL10n */
5+
/* global ajaxurl, pwsL10n, userProfileL10n */
66
(function($) {
77
var updateLock = false,
88
__ = wp.i18n.__,
@@ -91,6 +91,68 @@
9191
});
9292
}
9393

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 );
154+
}
155+
94156
function bindPasswordForm() {
95157
var $generateButton,
96158
$cancelButton;
@@ -369,6 +431,7 @@
369431
});
370432

371433
bindPasswordForm();
434+
bindPasswordRestLink();
372435
});
373436

374437
$( '#destroy-sessions' ).on( 'click', function( e ) {

src/wp-admin/admin-ajax.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
'health-check-loopback-requests',
141141
'health-check-get-sizes',
142142
'toggle-auto-updates',
143+
'send-password-reset',
143144
);
144145

145146
// Deprecated.

src/wp-admin/includes/ajax-actions.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5398,3 +5398,33 @@ function wp_ajax_toggle_auto_updates() {
53985398

53995399
wp_send_json_success();
54005400
}
5401+
5402+
/**
5403+
* Ajax handler sends a password reset link.
5404+
*
5405+
* @since 5.7.0
5406+
*/
5407+
function 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+
}

src/wp-admin/includes/class-wp-users-list-table.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ protected function get_bulk_actions() {
274274
}
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
}
279284

@@ -469,6 +474,11 @@ public function single_row( $user_object, $style = '', $role = '', $numposts = 0
469474
);
470475
}
471476

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>';
480+
}
481+
472482
/**
473483
* Filters the action links displayed under each user in the Users list table.
474484
*

src/wp-admin/user-edit.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,27 @@
609609
</td>
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
614635
/**

src/wp-admin/users.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,46 @@
208208
wp_redirect( $redirect );
209209
exit;
210210

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',
245+
),
246+
$redirect
247+
);
248+
wp_redirect( $redirect );
249+
exit;
250+
211251
case 'delete':
212252
if ( is_multisite() ) {
213253
wp_die( __( 'User deletion is not allowed from this screen.' ), 400 );
@@ -504,6 +544,16 @@
504544
);
505545
}
506546

547+
$messages[] = '<div id="message" class="updated notice is-dismissible"><p>' . $message . '</p></div>';
548+
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+
}
507557
$messages[] = '<div id="message" class="updated notice is-dismissible"><p>' . $message . '</p></div>';
508558
break;
509559
case 'promote':

0 commit comments

Comments
 (0)