*/ private $visible = array(); /** * Constructor. * * @param ProfileRepository $profiles Site storage. */ public function __construct( ProfileRepository $profiles ) { $this->profiles = $profiles; } /** * Whether a user can manage the plugin. * * @param int $user_id User ID. * @return bool */ public function can_manage( $user_id ) { return $user_id > 0 && user_can( $user_id, Capabilities::manage_primitive() ); } /** * Whether a user can view a site. * * @param int $user_id User ID. * @param string $site_id Site ID. * @return bool */ public function can_view( $user_id, $site_id ) { return in_array( (string) $site_id, $this->visible_ids( $user_id ), true ); } /** * Whether a user can view at least one site, or manage the plugin. * * @param int $user_id User ID. * @return bool */ public function has_any_access( $user_id ) { return $this->can_manage( $user_id ) || array() !== $this->visible_ids( $user_id ); } /** * Sites a user can view. * * @param int $user_id User ID. * @return Profile[] Keyed by ID, sorted by name. */ public function visible_sites( $user_id ) { $ids = array_flip( $this->visible_ids( $user_id ) ); $sites = array_intersect_key( $this->profiles->all(), $ids ); // Alphabetical, so the sidebar, the report's site menu and its default all agree. uasort( $sites, static function ( Profile $a, Profile $b ) { return strnatcasecmp( $a->display_name(), $b->display_name() ); } ); return $sites; } /** * Site IDs granted directly to a user. * * @param int $user_id User ID. * @return string[] */ public function direct_grants( $user_id ) { return array_map( 'strval', (array) get_user_meta( $user_id, $this->meta_key(), false ) ); } /** * IDs of users granted a site directly. * * @param string $site_id Site ID. * @return int[] */ public function users_for_site( $site_id ) { $ids = array(); $offset = 0; do { $page = get_users( array( 'meta_key' => $this->meta_key(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Indexed meta_key lookup on an admin screen. 'meta_value' => (string) $site_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Exact match, admin screen only. 'fields' => 'ID', 'number' => 500, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', ) ); $ids = array_merge( $ids, array_map( 'intval', $page ) ); $offset += 500; $count = count( $page ); } while ( 500 === $count ); return array_values( array_unique( $ids ) ); } /** * Replaces the list of users granted a site directly. * * @param string $site_id Site ID. * @param int[] $user_ids Users who should have access. * @return void */ public function set_users_for_site( $site_id, array $user_ids ) { $site_id = (string) $site_id; $want = array_unique( array_filter( array_map( 'intval', $user_ids ) ) ); $have = $this->users_for_site( $site_id ); foreach ( array_diff( $have, $want ) as $user_id ) { delete_user_meta( $user_id, $this->meta_key(), $site_id ); } foreach ( array_diff( $want, $have ) as $user_id ) { if ( get_userdata( $user_id ) && is_user_member_of_blog( $user_id ) ) { add_user_meta( $user_id, $this->meta_key(), $site_id ); } } $this->visible = array(); } /** * Removes every direct grant for a deleted site. * * @param string $site_id Site ID. * @return void */ public function forget_site( $site_id ) { delete_metadata( 'user', 0, $this->meta_key(), (string) $site_id, true ); $this->visible = array(); } /** * Roles that can be granted access, as slug => translated name. * * Administrators are left out because they always have access. * * @return array */ public function grantable_roles() { $roles = array(); foreach ( wp_roles()->roles as $slug => $role ) { if ( ! empty( $role['capabilities'][ Capabilities::manage_primitive() ] ) ) { continue; } $roles[ $slug ] = translate_user_role( $role['name'] ); } return $roles; } /** * User-meta key for direct grants, prefixed per blog on Multisite. * * @return string */ public function meta_key() { global $wpdb; return $wpdb->get_blog_prefix() . 'turtlereport_site'; } /** * Computes visible site IDs for a user. * * @param int $user_id User ID. * @return string[] */ private function visible_ids( $user_id ) { $user_id = (int) $user_id; if ( $user_id <= 0 ) { return array(); } $cache_key = get_current_blog_id() . '|' . $user_id; if ( isset( $this->visible[ $cache_key ] ) ) { return $this->visible[ $cache_key ]; } $all = $this->profiles->all(); if ( $this->can_manage( $user_id ) ) { $ids = array_keys( $all ); } elseif ( ! user_can( $user_id, 'read' ) ) { // No role on this site (or suspended): grants stay recorded but give no access, // so scheduled email audiences match the live permission check. $ids = array(); } else { $user = get_userdata( $user_id ); $roles = $user ? (array) $user->roles : array(); $direct = array_flip( $this->direct_grants( $user_id ) ); $ids = array(); foreach ( $all as $id => $profile ) { if ( isset( $direct[ $id ] ) || array_intersect( $roles, $profile->roles ) ) { $ids[] = (string) $id; } } } /** * Filters the site IDs a user can view. * * Use this to plug in custom rules (for example, membership levels). * The REST API enforces the result on every data request. * * @param string[] $ids Visible site IDs. * @param int $user_id User ID. */ $ids = (array) apply_filters( 'turtlereport_visible_site_ids', array_map( 'strval', $ids ), $user_id ); $this->visible[ $cache_key ] = array_values( array_intersect( array_map( 'strval', $ids ), array_map( 'strval', array_keys( $all ) ) ) ); return $this->visible[ $cache_key ]; } }