Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
342f997
Update heading
ocean90 Jul 15, 2025
58b709f
Remove default selected option
ocean90 Jul 15, 2025
fcb23c1
Typecast allusers parameter and reuse variable
ocean90 Jul 15, 2025
6328326
Add TODO
ocean90 Jul 15, 2025
abc9cc9
Update variable
ocean90 Jul 15, 2025
1ac17bd
Exclude user to delete from reassignment dropdown
ocean90 Jul 15, 2025
908a25c
Add ID to submit button
ocean90 Jul 15, 2025
c2b7fbd
Add TODO
ocean90 Jul 15, 2025
b366373
Disable button if nothing is selected
ocean90 Jul 15, 2025
1d4cd34
Use wp_dropdown_users()
ocean90 Jul 15, 2025
87143da
Use required attribute
ocean90 Jul 15, 2025
4e057d0
Refactor user deletion form, improve UI.
derpaschi Sep 16, 2025
afe1012
Implement logic to disable submit button until all radio buttons are …
derpaschi Oct 14, 2025
9d5385d
Check if form exists
ocean90 Nov 11, 2025
0add334
Refactor IIFE in common.js
ocean90 Nov 11, 2025
4fbfce2
Removed unused function parameter
derpaschi Nov 11, 2025
271ca0a
Update radio group selection in form validation
derpaschi Nov 11, 2025
c3e3a89
When deleting users, check if user has content, otherwise hide attrib…
derpaschi Nov 11, 2025
a2798e8
Remove duplicate filter documentation
ocean90 Nov 11, 2025
ef2fc0f
Fix: added missing comment closing tag
derpaschi Nov 11, 2025
77b3528
Merge branch 'trunk' into feature/deleting-users-56914
ocean90 May 12, 2026
4f586b5
Update deprecation version for `delete_users_add_js()` function to 7.…
derpaschi May 12, 2026
47856ac
Remove invalid legends and include user logins in legends
ocean90 May 12, 2026
57f2ac2
Update labels for clarity and accessibility
ocean90 May 12, 2026
37ad656
Fix HTML structure in confirm_delete_users() by removing an extra clo…
ocean90 May 12, 2026
a7d3ac9
Fix PHPCS warnings
ocean90 May 12, 2026
93b4b7b
Prevent user deletion without reassignment
ocean90 May 12, 2026
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
56 changes: 56 additions & 0 deletions src/js/_enqueues/admin/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -2356,3 +2356,59 @@ $( function( $ ) {
// Expose public methods.
return pub;
})();

/**
* Disable the submit button until all users radio buttons are checked.
*
* @since 7.0.0
*/
(function(){
const usersForm = document.querySelector( '.users-php .delete-and-reassign-users-form' );

// Check if the form exists and contains any radio buttons.
if ( ! usersForm || ! usersForm.querySelector( 'input[type="radio"]' ) ) {
return;
}

const submitBtn = usersForm.querySelector( 'input[type="submit"]' );

// Disable the submit button until all users radio buttons are checked.
submitBtn.disabled = true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For accessibility, it would be preferable to throw an error if not all radio decisions have been resolved instead of disabling the submit button. Disabled buttons aren't discoverable by assistive technology, so it becomes difficult for a screen reader user to identify what's missing.

The error would need to notify the user with useful information, e.g. how many items still need to be completed, and ideally would also provide easy access to reach the incomplete fields. (Which can be a pain if you have three missing decisions but 100 users to sort through.)


// Listen for changes on any radio input in the form.
usersForm.addEventListener('change', function() {
if ( ! usersForm.checkValidity() ) {
submitBtn.disabled = true;
return;
}

// Check all radio groups for validity.
let allValid = true;
const radioGroups = usersForm.querySelectorAll( 'fieldset ul' );
radioGroups.forEach( function( radioGroup ) {
const radios = radioGroup.querySelectorAll( 'input[type="radio"]' );
let checkedRadio = null;
radios.forEach( function( radio ) {
if ( radio.checked ) {
checkedRadio = radio;
}
});

if ( checkedRadio && checkedRadio.value === 'reassign' ) {
const select = radioGroup.querySelector( 'select' );
if ( select && select.value === '-1' ) {
allValid = false;
}
}
});

submitBtn.disabled = !allValid;
});

usersForm.querySelectorAll( 'select' ).forEach( function( selectElement ) {
selectElement.addEventListener( 'change', function( e ) {
const radio = e.target.closest( 'li' ).querySelector( 'input[type="radio"]' );
radio.checked = e.target.value !== '-1';
});
});
})();
11 changes: 11 additions & 0 deletions src/wp-admin/includes/deprecated.php
Original file line number Diff line number Diff line change
Expand Up @@ -1589,3 +1589,14 @@ function image_attachment_fields_to_save( $post, $attachment ) {

return $post;
}

/**
* Was used to add JavaScript to the delete users form.
*
* @since 3.5.0
* @deprecated 7.0.0
* @access private
*/
function delete_users_add_js() {
_deprecated_function( __FUNCTION__, '7.0.0' );
}
125 changes: 84 additions & 41 deletions src/wp-admin/includes/ms.php
Original file line number Diff line number Diff line change
Expand Up @@ -856,30 +856,31 @@ function _thickbox_path_admin_subfolder() {
* @return bool
*/
function confirm_delete_users( $users ) {
global $wpdb;

$current_user = wp_get_current_user();
if ( ! is_array( $users ) || empty( $users ) ) {
return false;
}

?>
<h1><?php esc_html_e( 'Users' ); ?></h1>
<h1><?php esc_html_e( 'Delete Users' ); ?></h1>

<?php if ( 1 === count( $users ) ) : ?>
<p><?php _e( 'You have chosen to delete the user from all networks and sites.' ); ?></p>
<?php else : ?>
<p><?php _e( 'You have chosen to delete the following users from all networks and sites.' ); ?></p>
<?php endif; ?>

<form action="users.php?action=dodelete" method="post">
<form action="users.php?action=dodelete" method="post" class="delete-and-reassign-users-form">
<input type="hidden" name="dodelete" />
<?php
wp_nonce_field( 'ms-users-delete' );
$site_admins = get_super_admins();
$admin_out = '<option value="' . esc_attr( $current_user->ID ) . '">' . $current_user->user_login . '</option>';
?>
<table class="form-table" role="presentation">
<?php
$allusers = (array) $_POST['allusers'];
foreach ( $allusers as $user_id ) {
foreach ( $users as $user_id ) {
if ( '' !== $user_id && '0' !== $user_id ) {
$delete_user = get_userdata( $user_id );

Expand All @@ -902,6 +903,7 @@ function confirm_delete_users( $users ) {
)
);
}

?>
<tr>
<th scope="row"><?php echo $delete_user->user_login; ?>
Expand All @@ -912,60 +914,101 @@ function confirm_delete_users( $users ) {

if ( ! empty( $blogs ) ) {
?>
<td><fieldset><p><legend>
<td><fieldset><legend>
<?php
printf(
/* translators: %s: User login. */
__( 'What should be done with content owned by %s?' ),
'<em>' . $delete_user->user_login . '</em>'
__( '%s: What should be done with the content owned by this user?' ),
$delete_user->user_login
);
?>
</legend></p>
</legend>
<?php
foreach ( (array) $blogs as $key => $details ) {
$blog_users = get_users(
array(
'blog_id' => $details->userblog_id,
'fields' => array( 'ID', 'user_login' ),
'fields' => array( 'ID' ),
'exclude' => $users,
)
);

$blog_users = wp_list_pluck( $blog_users, 'ID' );

if ( is_array( $blog_users ) && ! empty( $blog_users ) ) {
$user_site = "<a href='" . esc_url( get_home_url( $details->userblog_id ) ) . "'>{$details->blogname}</a>";
$user_dropdown = '<label for="reassign_user" class="screen-reader-text">' .
/* translators: Hidden accessibility text. */
__( 'Select a user' ) .
'</label>';
$user_dropdown .= "<select name='blog[$user_id][$key]' id='reassign_user'>";
$user_list = '';

foreach ( $blog_users as $user ) {
if ( ! in_array( (int) $user->ID, $allusers, true ) ) {
$user_list .= "<option value='{$user->ID}'>{$user->user_login}</option>";
$user_site = "<a href='" . esc_url( get_home_url( $details->userblog_id ) ) . "'>{$details->blogname}</a>";
switch_to_blog( $details->userblog_id );
/* This filter is documented in wp-admin/users.php */
$user_has_content = (bool) apply_filters( 'users_have_additional_content', false, array( $delete_user->ID ) );

if ( ! $user_has_content ) {
if ( $wpdb->get_var(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_author = %d
LIMIT 1",
$delete_user->ID
)
) ) {
$user_has_content = true;
} elseif ( $wpdb->get_var(
$wpdb->prepare(
"SELECT link_id FROM {$wpdb->links}
WHERE link_owner = %d
LIMIT 1",
$delete_user->ID
)
) ) {
$user_has_content = true;
}
}

if ( '' === $user_list ) {
$user_list = $admin_out;
}

$user_dropdown .= $user_list;
$user_dropdown .= "</select>\n";
?>
<ul style="list-style:none;">
<li>
restore_current_blog();

if ( ! $user_has_content ) {
?>
<p>
<?php
/* translators: %s: Link to user's site. */
printf( __( 'Site: %s' ), $user_site );
?>
</p>
<input type="hidden" id="delete_option_<?php echo esc_attr( $details->userblog_id . '_' . $delete_user->ID ); ?>" name="delete[<?php echo $details->userblog_id . '][' . $delete_user->ID; ?>]" value="delete" required />
<p><?php _e( 'This user does not have any content.' ); ?></p>
<?php
} else {
?>
<p>
Comment thread
ocean90 marked this conversation as resolved.
<?php
/* translators: %s: Link to user's site. */
printf( __( 'Site: %s' ), $user_site );
?>
</li>
<li><label><input type="radio" id="delete_option0" name="delete[<?php echo $details->userblog_id . '][' . $delete_user->ID; ?>]" value="delete" checked="checked" />
<?php _e( 'Delete all content.' ); ?></label></li>
<li><label><input type="radio" id="delete_option1" name="delete[<?php echo $details->userblog_id . '][' . $delete_user->ID; ?>]" value="reassign" />
<?php _e( 'Attribute all content to:' ); ?></label>
<?php echo $user_dropdown; ?></li>
</ul>
<?php
</p>
<ul>
<li>
<input type="radio" id="delete_option_<?php echo esc_attr( $details->userblog_id . '_' . $delete_user->ID ); ?>" name="delete[<?php echo $details->userblog_id . '][' . $delete_user->ID; ?>]" value="delete" required />
<label for="delete_option_<?php echo esc_attr( $details->userblog_id . '_' . $delete_user->ID ); ?>"><?php _e( 'Delete all content.' ); ?></label>
</li>
<li>
<input type="radio" id="reassign_option_<?php echo esc_attr( $details->userblog_id . '_' . $delete_user->ID ); ?>" name="delete[<?php echo $details->userblog_id . '][' . $delete_user->ID; ?>]" value="reassign" required />
<label for="reassign_option_<?php echo esc_attr( $details->userblog_id . '_' . $delete_user->ID ); ?>"><?php _e( 'Attribute all content to another user.' ); ?></label>

<label for="reassign_user_<?php echo esc_attr( $details->userblog_id . '_' . $delete_user->ID ); ?>" class="screen-reader-text"><?php _e( 'Select a user to attribute the content to.' ); ?></label>
<?php
wp_dropdown_users(
array(
'show_option_none' => __( 'Select a user' ),
'name' => "blog[$user_id][$key]",
'include' => $blog_users,
'show' => 'display_name_with_login',
'id' => "reassign_user_{$details->userblog_id}_{$delete_user->ID}",
)
);
?>

</li>
</ul>
<?php
}
}
}
echo '</fieldset></td></tr>';
Expand All @@ -982,7 +1025,7 @@ function confirm_delete_users( $users ) {
</table>
<?php
/** This action is documented in wp-admin/users.php */
do_action( 'delete_user_form', $current_user, $allusers );
do_action( 'delete_user_form', $current_user, $users );

if ( 1 === count( $users ) ) :
?>
Expand All @@ -992,7 +1035,7 @@ function confirm_delete_users( $users ) {
<?php
endif;

submit_button( __( 'Confirm Deletion' ), 'primary' );
submit_button( __( 'Confirm Deletion' ), 'primary', 'submit', false, array( 'id' => 'confirm-users-deletion' ) );
?>
</form>
<?php
Expand Down
20 changes: 0 additions & 20 deletions src/wp-admin/includes/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -561,26 +561,6 @@ function default_password_nag() {
);
}

/**
* @since 3.5.0
* @access private
*/
function delete_users_add_js() {
?>
<script>
jQuery( function($) {
var submit = $('#submit').prop('disabled', true);
$('input[name="delete_option"]').one('change', function() {
submit.prop('disabled', false);
});
$('#reassign_user').focus( function() {
$('#delete_option1').prop('checked', true).trigger('change');
});
} );
</script>
<?php
}

/**
* Optional SSL preference that can be turned on by hooking to the 'personal_options' action.
*
Expand Down
5 changes: 3 additions & 2 deletions src/wp-admin/network/users.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@

$doaction = $_POST['action'];
$userfunction = '';
$allusers = (array) $_POST['allusers'];

foreach ( (array) $_POST['allusers'] as $user_id ) {
foreach ( $allusers as $user_id ) {
if ( ! empty( $user_id ) ) {
switch ( $doaction ) {
case 'delete':
Expand All @@ -72,7 +73,7 @@
require_once ABSPATH . 'wp-admin/admin-header.php';

echo '<div class="wrap">';
confirm_delete_users( $_POST['allusers'] );
confirm_delete_users( $allusers );
echo '</div>';

require_once ABSPATH . 'wp-admin/admin-footer.php';
Expand Down
Loading
Loading