diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 9c635f63d288a..f6a0a77d9df08 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -1152,6 +1152,7 @@ function get_blogs_of_user( $user_id, $all = false ) { * Finds out whether a user is a member of a given blog. * * @since MU (3.0.0) + * @since 7.1.0 Introduced the `is_user_member_of_blog` filter. * * @global wpdb $wpdb WordPress database abstraction object. * @@ -1201,9 +1202,23 @@ function is_user_member_of_blog( $user_id = 0, $blog_id = 0 ) { } else { $capabilities_key = $wpdb->base_prefix . $blog_id . '_capabilities'; } - $has_cap = get_user_meta( $user_id, $capabilities_key, true ); + $has_cap = get_user_meta( $user_id, $capabilities_key, true ); + $is_member = is_array( $has_cap ); - return is_array( $has_cap ); + /** + * Filters whether a user is a member of a given blog. + * + * This filter only runs when the user and blog have both been resolved + * to valid records on a multisite install; it is not invoked for logged-out + * requests, unknown users, or archived/spammed/deleted sites. + * + * @since 7.1.0 + * + * @param bool $is_member Whether the user is a member of the blog. + * @param int $user_id The user ID being checked. + * @param int $blog_id The blog ID being checked. + */ + return (bool) apply_filters( 'is_user_member_of_blog', $is_member, $user_id, $blog_id ); } /** diff --git a/tests/phpunit/tests/user/multisite.php b/tests/phpunit/tests/user/multisite.php index d01856cb14c2f..c6126b358b87f 100644 --- a/tests/phpunit/tests/user/multisite.php +++ b/tests/phpunit/tests/user/multisite.php @@ -176,6 +176,73 @@ public function test_is_user_member_of_blog() { wp_set_current_user( $old_current ); } + /** + * Ensures the `is_user_member_of_blog` filter can override the return value + * and receives the resolved user ID and blog ID. + * + * @ticket 65096 + * + * @covers ::is_user_member_of_blog + */ + public function test_is_user_member_of_blog_filter() { + $user_id = self::factory()->user->create(); + $blog_id = self::factory()->blog->create(); + + // Sanity check: the user is not a member of the blog by default. + $this->assertFalse( is_user_member_of_blog( $user_id, $blog_id ) ); + + $filter_args = array(); + $filter = function ( $is_member, $filtered_user_id, $filtered_blog_id ) use ( &$filter_args ) { + $filter_args[] = array( $is_member, $filtered_user_id, $filtered_blog_id ); + return true; + }; + + add_filter( 'is_user_member_of_blog', $filter, 10, 3 ); + $result = is_user_member_of_blog( $user_id, $blog_id ); + remove_filter( 'is_user_member_of_blog', $filter, 10 ); + + $this->assertTrue( $result, 'Filter should be able to force a truthy return value.' ); + $this->assertCount( 1, $filter_args, 'Filter should run exactly once per call on a valid multisite blog.' ); + $this->assertSame( array( false, $user_id, $blog_id ), $filter_args[0], 'Filter should receive the computed membership, user ID, and blog ID.' ); + } + + /** + * Ensures the `is_user_member_of_blog` filter is not invoked for requests + * that short-circuit before the membership is computed. + * + * @ticket 65096 + * + * @covers ::is_user_member_of_blog + */ + public function test_is_user_member_of_blog_filter_not_called_for_invalid_input() { + $filter_calls = 0; + $filter = function ( $is_member ) use ( &$filter_calls ) { + ++$filter_calls; + return $is_member; + }; + + add_filter( 'is_user_member_of_blog', $filter ); + + // No current user, and no user ID provided. + $old_current = get_current_user_id(); + wp_set_current_user( 0 ); + $this->assertFalse( is_user_member_of_blog() ); + + // Unknown user ID. + $this->assertFalse( is_user_member_of_blog( PHP_INT_MAX ) ); + + // Known user, but an archived/deleted/spam site short-circuits. + $user_id = self::factory()->user->create(); + $blog_id = self::factory()->blog->create(); + update_blog_details( $blog_id, array( 'archived' => 1 ) ); + $this->assertFalse( is_user_member_of_blog( $user_id, $blog_id ) ); + + wp_set_current_user( $old_current ); + remove_filter( 'is_user_member_of_blog', $filter ); + + $this->assertSame( 0, $filter_calls, 'Filter should not run when the function short-circuits before computing membership.' ); + } + /** * @ticket 23192 */