Make WordPress Core

Opened 8 days ago

Last modified 5 hours ago

#66012 reopened defect (bug)

Multisite: reassign dropdown missing when deleting a user, silently destroying content (7.1 regression)

Reported by: mgyura Owned by: joedolson
Priority: normal Milestone: 7.1.1
Component: Networks and Sites Version: 7.1
Severity: major Keywords: has-patch has-unit-tests dev-feedback fixed-major
Cc: Focuses: multisite

Description

On Multisite in 7.1, the Network Admin "Delete Users" screen offers "Attribute all
content to another user", but on most networks the dropdown of users to attribute the
content to is missing entirely. Choosing that option deletes the content anyway, with
no error and no warning.

This is a regression introduced in 7.1 by [62688] (#56914). 7.0 and earlier are not
affected.

Impact

When the dropdown is missing, both options on the screen destroy the content:

  • "Delete all content." deletes it, as expected.
  • "Attribute all content to another user." also deletes it, because no reassignment ever happens.

The missing_reassign guard added in the same changeset cannot catch this. It iterates
$_POST['blog'], and when no dropdown is rendered there is no blog[...] field to
submit. So the guard is skipped, remove_user_from_blog() is never called with a
reassign target, and wpmu_delete_user() then runs wp_delete_post() over every post
the user owns.

On a production network, this destroyed 2,059 posts and 2,668 media files on one subsite.
The administrator had deliberately chosen "Attribute all content to another user" and
expected to be asked for a target.

Steps to reproduce

  1. Multisite on 7.1.
  2. Create a subsite.
  3. Create a user and add them to that subsite only. Do not add them to the main site. This is the key condition: a user who also belongs to the main site will not reproduce it.
  4. Give that user a post on the subsite.
  5. Network Admin > Users > select the user > Delete.

Expected: a dropdown listing the subsite's other users to reassign the content to.

Actual: the "Attribute all content to another user" radio appears with no dropdown
beside it. Selecting it and confirming deletes the user's content.

Root cause

In confirm_delete_users() (wp-admin/includes/ms.php), the blog context is restored
before the dropdown is rendered, and wp_dropdown_users() is not given a blog_id:

switch_to_blog( $details->userblog_id );
$user_has_content = ...
restore_current_blog();          // context dropped here
...
wp_dropdown_users(
    array(
        'show_option_none' => __( 'Select a user' ),
        'name'    => "blog[$user_id][$key]",
        'include' => $blog_users,          // members of the subsite
        'show'    => 'display_name_with_login',
        'id'      => "reassign_user_{$details->userblog_id}_{$delete_user->ID}",
    )
);

blog_id therefore falls back to get_current_blog_id(), which in Network Admin is the
network's main site. On Multisite, any non-zero blog_id makes WP_User_Query add a
capabilities meta query for that blog. The query then asks for users belonging to the
main site, while include restricts the results to members of the subsite. Where those
two sets do not overlap the result is empty, and wp_dropdown_users() returns an empty
string instead of a dropdown.

#30175 describes this empty-string behavior in wp_dropdown_users() itself. This is a
new caller running into it.

Why 7.0 was not affected

7.0 built the dropdown by hand and handled the empty case explicitly:

if ( '' === $user_list ) {
    $user_list = $admin_out;   // fall back to the current super admin
}

[62688] replaced that markup with wp_dropdown_users(), and the fallback was not
carried over.

Suggested patch

Pass the subsite explicitly:

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}",
        'blog_id' => $details->userblog_id,
    )
);

Rendering the dropdown before restore_current_blog() would work as well.

The validation is worth hardening separately. It should key off the delete[...] radio
rather than the presence of blog[...], so that "reassign" chosen with no submitted
target is rejected instead of silently proceeding to deletion.

  • #56914 and [62688] introduced this.
  • #30175 wp_dropdown_users() returning an empty string on Multisite.
  • #23361 covers the dangerous default on this screen, which is a separate issue.

Attachments (1)

66012.diff (361 bytes ) - added by mgyura 8 days ago.
Patch attached. It passes the subsite explicitly, so wp_dropdown_users() no longer falls back to the network's main site. Tested on a 74-site network running 7.1: the dropdown went from rendering nothing at all to listing all six members of the subsite, and an ordinary author dropdown elsewhere in the admin was unaffected. Also confirmed the equivalent call returns the correct members for a second subsite.

Download all attachments as: .zip

Change History (12)

#1 @johnbillion
8 days ago

  • Milestone Awaiting Review7.1.1

@mgyura
8 days ago

Patch attached. It passes the subsite explicitly, so wp_dropdown_users() no longer falls back to the network's main site. Tested on a 74-site network running 7.1: the dropdown went from rendering nothing at all to listing all six members of the subsite, and an ordinary author dropdown elsewhere in the admin was unaffected. Also confirmed the equivalent call returns the correct members for a second subsite.

#2 @johnbillion
8 days ago

  • Keywords has-patch needs-unit-tests added

#3 @joedolson
8 days ago

  • Owner set to joedolson
  • Status newaccepted

This ticket was mentioned in PR #13391 on WordPress/wordpress-develop by @joedolson.


6 days ago
#4

  • Keywords has-unit-tests added; needs-unit-tests removed

Applies patch by https://profiles.wordpress.org/mgyura/ and adds related tests.

Trac ticket: https://core.trac.wordpress.org/ticket/66012

## Use of AI Tools

AI assistance: Yes
Tool(s): GitHub Copilot, Claude
Model(s): Sonnet 5
Used for: Test framing; reviewed by me.

This ticket was mentioned in Slack in #core by adrianduffell. View the logs.


5 days ago

This ticket was mentioned in Slack in #core by adamsilverstein. View the logs.


11 hours ago

#7 @adamsilverstein
11 hours ago

  • Keywords commit added

#8 @joedolson
10 hours ago

  • Component GeneralNetworks and Sites
  • Focuses multisite added

#9 @joedolson
10 hours ago

  • Resolutionfixed
  • Status acceptedclosed

In 63544:

Multisite: Restore missing user reassignment dropdown when deleting user.

Explicitly pass the blog_id attribute to wp_dropdown_users in confirm_delete_users() so that the dropdown renders the list of users appropriate to the current site, rather than attempting to fallback to the main site despite the limits set by the include parameter, which limited to members of the subsite. This resulted in an empty list in any case where there was no overlap between members of the current subset and members of the primary network site.

Props mgyura, johnbillion, adamsilverstein, joedolson.
Fixes #66012.

#10 @joedolson
10 hours ago

  • Keywords dev-feedback fixed-major added; commit removed
  • Resolution fixed
  • Status closedreopened

Reopening for 2nd committer review anbd merge consideration to 7.1.1.

#11 @adrianduffell
5 hours ago

Let's proceed with preparing this for 7.1.1. It is testing well for me on trunk.

Note: See TracTickets for help on using tickets.