Skip to content

Commit 1e20673

Browse files
author
user
committed
Role/Capability: Add is_user_member_of_blog filter.
Allows plugins to dynamically grant or revoke a user's membership of a blog without having to spoof `{$prefix}capabilities` user meta. The filter only runs when both the user and blog have resolved to valid records on a multisite install, so short-circuited calls (logged-out requests, unknown users, archived/spammed/deleted sites) continue to return without invoking the filter. Props dd32. Fixes #65096.
1 parent 7f83643 commit 1e20673

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

src/wp-includes/user.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,7 @@ function get_blogs_of_user( $user_id, $all = false ) {
11521152
* Finds out whether a user is a member of a given blog.
11531153
*
11541154
* @since MU (3.0.0)
1155+
* @since 7.1.0 Introduced the `is_user_member_of_blog` filter.
11551156
*
11561157
* @global wpdb $wpdb WordPress database abstraction object.
11571158
*
@@ -1201,9 +1202,23 @@ function is_user_member_of_blog( $user_id = 0, $blog_id = 0 ) {
12011202
} else {
12021203
$capabilities_key = $wpdb->base_prefix . $blog_id . '_capabilities';
12031204
}
1204-
$has_cap = get_user_meta( $user_id, $capabilities_key, true );
1205+
$has_cap = get_user_meta( $user_id, $capabilities_key, true );
1206+
$is_member = is_array( $has_cap );
12051207

1206-
return is_array( $has_cap );
1208+
/**
1209+
* Filters whether a user is a member of a given blog.
1210+
*
1211+
* This filter only runs when the user and blog have both been resolved
1212+
* to valid records on a multisite install; it is not invoked for logged-out
1213+
* requests, unknown users, or archived/spammed/deleted sites.
1214+
*
1215+
* @since 7.1.0
1216+
*
1217+
* @param bool $is_member Whether the user is a member of the blog.
1218+
* @param int $user_id The user ID being checked.
1219+
* @param int $blog_id The blog ID being checked.
1220+
*/
1221+
return (bool) apply_filters( 'is_user_member_of_blog', $is_member, $user_id, $blog_id );
12071222
}
12081223

12091224
/**

tests/phpunit/tests/user/multisite.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,73 @@ public function test_is_user_member_of_blog() {
176176
wp_set_current_user( $old_current );
177177
}
178178

179+
/**
180+
* Ensures the `is_user_member_of_blog` filter can override the return value
181+
* and receives the resolved user ID and blog ID.
182+
*
183+
* @ticket 65096
184+
*
185+
* @covers ::is_user_member_of_blog
186+
*/
187+
public function test_is_user_member_of_blog_filter() {
188+
$user_id = self::factory()->user->create();
189+
$blog_id = self::factory()->blog->create();
190+
191+
// Sanity check: the user is not a member of the blog by default.
192+
$this->assertFalse( is_user_member_of_blog( $user_id, $blog_id ) );
193+
194+
$filter_args = array();
195+
$filter = function ( $is_member, $filtered_user_id, $filtered_blog_id ) use ( &$filter_args ) {
196+
$filter_args[] = array( $is_member, $filtered_user_id, $filtered_blog_id );
197+
return true;
198+
};
199+
200+
add_filter( 'is_user_member_of_blog', $filter, 10, 3 );
201+
$result = is_user_member_of_blog( $user_id, $blog_id );
202+
remove_filter( 'is_user_member_of_blog', $filter, 10 );
203+
204+
$this->assertTrue( $result, 'Filter should be able to force a truthy return value.' );
205+
$this->assertCount( 1, $filter_args, 'Filter should run exactly once per call on a valid multisite blog.' );
206+
$this->assertSame( array( false, $user_id, $blog_id ), $filter_args[0], 'Filter should receive the computed membership, user ID, and blog ID.' );
207+
}
208+
209+
/**
210+
* Ensures the `is_user_member_of_blog` filter is not invoked for requests
211+
* that short-circuit before the membership is computed.
212+
*
213+
* @ticket 65096
214+
*
215+
* @covers ::is_user_member_of_blog
216+
*/
217+
public function test_is_user_member_of_blog_filter_not_called_for_invalid_input() {
218+
$filter_calls = 0;
219+
$filter = function ( $is_member ) use ( &$filter_calls ) {
220+
++$filter_calls;
221+
return $is_member;
222+
};
223+
224+
add_filter( 'is_user_member_of_blog', $filter );
225+
226+
// No current user, and no user ID provided.
227+
$old_current = get_current_user_id();
228+
wp_set_current_user( 0 );
229+
$this->assertFalse( is_user_member_of_blog() );
230+
231+
// Unknown user ID.
232+
$this->assertFalse( is_user_member_of_blog( PHP_INT_MAX ) );
233+
234+
// Known user, but an archived/deleted/spam site short-circuits.
235+
$user_id = self::factory()->user->create();
236+
$blog_id = self::factory()->blog->create();
237+
update_blog_details( $blog_id, array( 'archived' => 1 ) );
238+
$this->assertFalse( is_user_member_of_blog( $user_id, $blog_id ) );
239+
240+
wp_set_current_user( $old_current );
241+
remove_filter( 'is_user_member_of_blog', $filter );
242+
243+
$this->assertSame( 0, $filter_calls, 'Filter should not run when the function short-circuits before computing membership.' );
244+
}
245+
179246
/**
180247
* @ticket 23192
181248
*/

0 commit comments

Comments
 (0)