From 745ff6636349ae088f0879494b5e75e46048baec Mon Sep 17 00:00:00 2001 From: Igor Rozum Date: Tue, 10 Nov 2026 11:07:58 -0500 Subject: [PATCH 1/4] Multisite: Extract reassignment of deleted users' content and add tests --- src/wp-admin/includes/ms.php | 25 +++ src/wp-admin/network/users.php | 14 +- .../wpmuReassignDeletedUsersContent.php | 185 ++++++++++++++++++ 3 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php diff --git a/src/wp-admin/includes/ms.php b/src/wp-admin/includes/ms.php index 8e3c141af2850..3f1ec5cc3a364 100644 --- a/src/wp-admin/includes/ms.php +++ b/src/wp-admin/includes/ms.php @@ -213,6 +213,31 @@ function wpmu_delete_user( $id ) { return true; } +/** + * Reassigns the content of users about to be deleted from the network. + * + * @since 7.3.0 + * @access private + * + * @param array $reassign_users Array of user IDs to reassign to, keyed by site ID, keyed by the ID of the user being deleted. + * @param array $content_actions Array of 'delete' or 'reassign' choices, keyed by the ID of the user being deleted, keyed by site ID. + */ +function _wpmu_reassign_deleted_users_content( $reassign_users, $content_actions ) { + foreach ( $reassign_users as $user_id => $sites ) { + if ( ! current_user_can( 'delete_user', $user_id ) ) { + continue; + } + + foreach ( $sites as $site_id => $reassign_user_id ) { + if ( isset( $content_actions[ $site_id ][ $user_id ] ) && 'reassign' === $content_actions[ $site_id ][ $user_id ] ) { + remove_user_from_blog( $user_id, $site_id, (int) $reassign_user_id ); + } else { + remove_user_from_blog( $user_id, $site_id ); + } + } + } +} + /** * Checks whether a site has used its allotted upload space. * diff --git a/src/wp-admin/network/users.php b/src/wp-admin/network/users.php index c7b63b4360295..52ce8cad45363 100644 --- a/src/wp-admin/network/users.php +++ b/src/wp-admin/network/users.php @@ -216,19 +216,7 @@ } if ( ! empty( $_POST['blog'] ) && is_array( $_POST['blog'] ) ) { - foreach ( $_POST['blog'] as $id => $users ) { - foreach ( $users as $blogid => $user_id ) { - if ( ! current_user_can( 'delete_user', $id ) ) { - continue; - } - - if ( ! empty( $_POST['delete'] ) && 'reassign' === $_POST['delete'][ $blogid ][ $id ] ) { - remove_user_from_blog( $id, $blogid, (int) $user_id ); - } else { - remove_user_from_blog( $id, $blogid ); - } - } - } + _wpmu_reassign_deleted_users_content( $_POST['blog'], ! empty( $_POST['delete'] ) ? $_POST['delete'] : array() ); } $i = 0; diff --git a/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php b/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php new file mode 100644 index 0000000000000..8ed782a35f616 --- /dev/null +++ b/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php @@ -0,0 +1,185 @@ +user->create(); + self::$author = $factory->user->create(); + self::$heir = $factory->user->create(); + grant_super_admin( self::$admin ); + + self::$site_b = $factory->blog->create(); + + foreach ( array( 1, self::$site_b ) as $site_id ) { + add_user_to_blog( $site_id, self::$author, 'author' ); + add_user_to_blog( $site_id, self::$heir, 'author' ); + } + } + + public static function wpTearDownAfterClass() { + revoke_super_admin( self::$admin ); + wpmu_delete_user( self::$admin ); + wpmu_delete_user( self::$heir ); + wpmu_delete_user( self::$author ); + wp_delete_site( self::$site_b ); + } + + public function set_up() { + parent::set_up(); + require_once ABSPATH . 'wp-admin/includes/ms.php'; + wp_set_current_user( self::$admin ); + } + + /** + * @ticket 17905 + */ + public function test_should_delete_posts_on_every_site_set_to_delete_content() { + $posts = $this->create_posts_on_both_sites(); + + $this->reassign_and_delete( + array( 1 => 'delete', self::$site_b => 'delete' ) + ); + + $this->assertNull( $this->get_post_on_site( 1, $posts[1] ) ); + $this->assertNull( $this->get_post_on_site( self::$site_b, $posts[ self::$site_b ] ) ); + } + + /** + * @ticket 17905 + */ + public function test_should_reassign_posts_on_reassigned_site_and_delete_them_elsewhere() { + $posts = $this->create_posts_on_both_sites(); + + $this->reassign_and_delete( + array( 1 => 'reassign', self::$site_b => 'delete' ) + ); + + $reassigned_post = $this->get_post_on_site( 1, $posts[1] ); + + $this->assertInstanceOf( WP_Post::class, $reassigned_post ); + $this->assertSame( (string) self::$heir, $reassigned_post->post_author ); + $this->assertNull( $this->get_post_on_site( self::$site_b, $posts[ self::$site_b ] ) ); + } + + /** + * @ticket 17905 + */ + public function test_should_keep_membership_of_sites_set_to_delete_content() { + $this->create_posts_on_both_sites(); + + _wpmu_reassign_deleted_users_content( + $this->build_reassign_users( array( 1 => 'delete', self::$site_b => 'delete' ) ), + $this->build_content_actions( array( 1 => 'delete', self::$site_b => 'delete' ) ) + ); + + $this->assertTrue( is_user_member_of_blog( self::$author, 1 ) ); + $this->assertTrue( is_user_member_of_blog( self::$author, self::$site_b ) ); + } + + /** + * @ticket 17905 + */ + public function test_should_not_reassign_content_of_users_the_current_user_cannot_delete() { + $posts = $this->create_posts_on_both_sites(); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) ); + + _wpmu_reassign_deleted_users_content( + $this->build_reassign_users( array( 1 => 'reassign' ) ), + $this->build_content_actions( array( 1 => 'reassign' ) ) + ); + + $post = $this->get_post_on_site( 1, $posts[1] ); + + $this->assertSame( (string) self::$author, $post->post_author ); + $this->assertTrue( is_user_member_of_blog( self::$author, 1 ) ); + } + + /** + * Creates one post by the author on the main site and on the second site. + * + * @return int[] Post IDs keyed by site ID. + */ + private function create_posts_on_both_sites() { + $posts = array(); + + foreach ( array( 1, self::$site_b ) as $site_id ) { + switch_to_blog( $site_id ); + $posts[ $site_id ] = self::factory()->post->create( array( 'post_author' => self::$author ) ); + restore_current_blog(); + } + + return $posts; + } + + /** + * Runs the same sequence as the network "Delete Users" handler. + * + * @param string[] $choices 'delete' or 'reassign' keyed by site ID. + */ + private function reassign_and_delete( $choices ) { + _wpmu_reassign_deleted_users_content( + $this->build_reassign_users( $choices ), + $this->build_content_actions( $choices ) + ); + + wpmu_delete_user( self::$author ); + } + + /** + * Builds the `blog` form field: the reassignment target keyed by user, keyed by site. + * + * @param string[] $choices 'delete' or 'reassign' keyed by site ID. + * @return array + */ + private function build_reassign_users( $choices ) { + $targets = array(); + + foreach ( $choices as $site_id => $choice ) { + $targets[ $site_id ] = 'reassign' === $choice ? (string) self::$heir : '-1'; + } + + return array( self::$author => $targets ); + } + + /** + * Builds the `delete` form field: the choice keyed by site, keyed by user. + * + * @param string[] $choices 'delete' or 'reassign' keyed by site ID. + * @return array + */ + private function build_content_actions( $choices ) { + $actions = array(); + + foreach ( $choices as $site_id => $choice ) { + $actions[ $site_id ] = array( self::$author => $choice ); + } + + return $actions; + } + + private function get_post_on_site( $site_id, $post_id ) { + switch_to_blog( $site_id ); + $post = get_post( $post_id ); + restore_current_blog(); + + return $post; + } + } + +endif; From 913e4b4d5ec8c08aa673f05dd47619d760ac30ba Mon Sep 17 00:00:00 2001 From: Igor Rozum Date: Tue, 10 Nov 2026 11:08:14 -0500 Subject: [PATCH 2/4] Multisite: Keep users on sites set to delete content so wpmu_delete_user() deletes it --- src/wp-admin/includes/ms.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/ms.php b/src/wp-admin/includes/ms.php index 3f1ec5cc3a364..0614c01007d9e 100644 --- a/src/wp-admin/includes/ms.php +++ b/src/wp-admin/includes/ms.php @@ -216,6 +216,9 @@ function wpmu_delete_user( $id ) { /** * Reassigns the content of users about to be deleted from the network. * + * Sites whose content is being deleted are left alone, because wpmu_delete_user() + * deletes the user's posts and links on every site the user still belongs to. + * * @since 7.3.0 * @access private * @@ -231,8 +234,6 @@ function _wpmu_reassign_deleted_users_content( $reassign_users, $content_actions foreach ( $sites as $site_id => $reassign_user_id ) { if ( isset( $content_actions[ $site_id ][ $user_id ] ) && 'reassign' === $content_actions[ $site_id ][ $user_id ] ) { remove_user_from_blog( $user_id, $site_id, (int) $reassign_user_id ); - } else { - remove_user_from_blog( $user_id, $site_id ); } } } From 02114506037bd3e73bbcc0a22b2cb0412f15bfe4 Mon Sep 17 00:00:00 2001 From: Igor Rozum Date: Tue, 10 Nov 2026 11:09:02 -0500 Subject: [PATCH 3/4] Tests: Assert content of deleted users is trashed on every site --- .../tests/multisite/wpmuReassignDeletedUsersContent.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php b/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php index 8ed782a35f616..9a9231a23a9b0 100644 --- a/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php +++ b/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php @@ -56,8 +56,8 @@ public function test_should_delete_posts_on_every_site_set_to_delete_content() { array( 1 => 'delete', self::$site_b => 'delete' ) ); - $this->assertNull( $this->get_post_on_site( 1, $posts[1] ) ); - $this->assertNull( $this->get_post_on_site( self::$site_b, $posts[ self::$site_b ] ) ); + $this->assertSame( 'trash', get_post_status( $this->get_post_on_site( 1, $posts[1] ) ) ); + $this->assertSame( 'trash', get_post_status( $this->get_post_on_site( self::$site_b, $posts[ self::$site_b ] ) ) ); } /** @@ -74,7 +74,7 @@ public function test_should_reassign_posts_on_reassigned_site_and_delete_them_el $this->assertInstanceOf( WP_Post::class, $reassigned_post ); $this->assertSame( (string) self::$heir, $reassigned_post->post_author ); - $this->assertNull( $this->get_post_on_site( self::$site_b, $posts[ self::$site_b ] ) ); + $this->assertSame( 'trash', get_post_status( $this->get_post_on_site( self::$site_b, $posts[ self::$site_b ] ) ) ); } /** From 28dfb49557b9a65ea43882d39f5eeda9476b92eb Mon Sep 17 00:00:00 2001 From: Igor Rozum Date: Tue, 10 Nov 2026 11:11:00 -0500 Subject: [PATCH 4/4] Tests: Fix coding standards in wpmuReassignDeletedUsersContent --- .../wpmuReassignDeletedUsersContent.php | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php b/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php index 9a9231a23a9b0..0036ce4b8c4bc 100644 --- a/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php +++ b/tests/phpunit/tests/multisite/wpmuReassignDeletedUsersContent.php @@ -53,7 +53,10 @@ public function test_should_delete_posts_on_every_site_set_to_delete_content() { $posts = $this->create_posts_on_both_sites(); $this->reassign_and_delete( - array( 1 => 'delete', self::$site_b => 'delete' ) + array( + 1 => 'delete', + self::$site_b => 'delete', + ) ); $this->assertSame( 'trash', get_post_status( $this->get_post_on_site( 1, $posts[1] ) ) ); @@ -67,7 +70,10 @@ public function test_should_reassign_posts_on_reassigned_site_and_delete_them_el $posts = $this->create_posts_on_both_sites(); $this->reassign_and_delete( - array( 1 => 'reassign', self::$site_b => 'delete' ) + array( + 1 => 'reassign', + self::$site_b => 'delete', + ) ); $reassigned_post = $this->get_post_on_site( 1, $posts[1] ); @@ -84,8 +90,18 @@ public function test_should_keep_membership_of_sites_set_to_delete_content() { $this->create_posts_on_both_sites(); _wpmu_reassign_deleted_users_content( - $this->build_reassign_users( array( 1 => 'delete', self::$site_b => 'delete' ) ), - $this->build_content_actions( array( 1 => 'delete', self::$site_b => 'delete' ) ) + $this->build_reassign_users( + array( + 1 => 'delete', + self::$site_b => 'delete', + ) + ), + $this->build_content_actions( + array( + 1 => 'delete', + self::$site_b => 'delete', + ) + ) ); $this->assertTrue( is_user_member_of_blog( self::$author, 1 ) );