namespace, '/spaces/(?P\d+)/members', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'permission_logged_in' ), 'args' => array_merge( $this->paging_args( 100 ), array( 'status' => array( 'type' => 'string', 'enum' => Membership::STATUSES, 'default' => 'active', ), ) ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'permission_logged_in' ), 'args' => array( 'user_id' => array( 'type' => 'integer', 'required' => true, ), 'mode' => array( 'type' => 'string', 'enum' => array( 'invite', 'add' ), 'default' => 'invite', ), ), ), ) ); register_rest_route( $this->namespace, '/spaces/(?P\d+)/members/(?P\d+)', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'permission_logged_in' ), 'args' => array( 'role' => array( 'type' => 'string', 'enum' => Membership::ROLES, ), 'status' => array( 'type' => 'string', 'enum' => array( 'active', 'blocked' ), ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'permission_logged_in' ), ), ) ); } /** * Lists members. Active members are visible to readers of the space; * requests, invitations and blocked members only to space admins. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function get_items( $request ) { $space = $this->visible_space( $request['id'] ); if ( is_wp_error( $space ) ) { return $space; } $user_id = get_current_user_id(); $status = $request['status']; $is_admin = SpaceAccess::can_manage( $space, $user_id ); $allowed = 'active' === $status ? SpaceAccess::can_read( $space, $user_id ) : $is_admin; // With the member directory off, only space admins see member lists. if ( ! $allowed || ( ! $is_admin && 'off' === Settings::get( 'member_directory' ) ) ) { return $this->forbidden(); } $result = Membership::list_members( $space['id'], $status, $request['page'], $request['per_page'], $request['search'] ); // Members who hid their profile, or wait for approval, are left out for everyone but space admins. if ( ! $is_admin ) { $excluded = array_diff( array_merge( Profiles::hidden_ids(), Profiles::inactive_ids() ), array( $user_id ) ); $result['items'] = array_values( array_filter( $result['items'], static function ( $row ) use ( $excluded ) { return ! in_array( (int) $row['user_id'], $excluded, true ); } ) ); $result['total'] = max( 0, $result['total'] - self::excluded_in_space( $space['id'], $status, $excluded ) ); } $ids = array_map( 'intval', wp_list_pluck( $result['items'], 'user_id' ) ); cache_users( $ids ); Profiles::get_many( $ids ); $items = array(); foreach ( $result['items'] as $row ) { $user = get_userdata( (int) $row['user_id'] ); if ( ! $user ) { continue; } $items[] = array_merge( MemberDirectory::format( $user ), array( 'space_role' => $row['role'], 'space_status' => $row['status'], 'member_since' => Time::iso( $row['created_at'] ), ) ); } return $this->paged_response( $items, $result['total'], $request['per_page'] ); } /** * Invites a user, or adds them directly (community managers only). * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function create_item( $request ) { $limited = \SpaceBay\Support\RateLimit::hit( 'invite', get_current_user_id(), 30 ); if ( is_wp_error( $limited ) ) { return $limited; } $space = $this->manageable_space( $request['id'] ); if ( is_wp_error( $space ) ) { return $space; } $target = get_userdata( absint( $request['user_id'] ) ); if ( ! $target || ( is_multisite() && ! is_user_member_of_blog( $target->ID ) ) ) { return new WP_Error( 'spacebay_user_not_found', __( 'Member not found.', 'spacebay' ), array( 'status' => 404 ) ); } $existing = Membership::get( $space['id'], $target->ID ); if ( $existing && 'active' === $existing['status'] ) { return new WP_Error( 'spacebay_already_member', __( 'This person is already a member.', 'spacebay' ), array( 'status' => 400 ) ); } $add = 'add' === $request['mode']; if ( $add && ! SpaceAccess::is_manager( get_current_user_id() ) ) { return $this->forbidden(); } Membership::set( $space['id'], $target->ID, $existing ? $existing['role'] : 'member', $add ? 'active' : 'invited' ); /** * Fires after a user is invited to or added to a space. * * @since 1.0.0 * * @param int $space_id Space ID. * @param int $user_id Invited user ID. * @param int $actor_id User who sent the invitation. * @param string $mode "invite" or "add". */ do_action( 'spacebay_space_member_invited', $space['id'], $target->ID, get_current_user_id(), $request['mode'] ); $response = rest_ensure_response( $this->membership_response( $space['id'], $target->ID ) ); $response->set_status( 201 ); return $response; } /** * Approves a request, blocks or unblocks, or changes a role. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function update_item( $request ) { $space = $this->manageable_space( $request['id'] ); if ( is_wp_error( $space ) ) { return $space; } $target_id = absint( $request['user_id'] ); $membership = Membership::get( $space['id'], $target_id ); if ( ! $membership ) { return new WP_Error( 'spacebay_not_member', __( 'This person is not in the space.', 'spacebay' ), array( 'status' => 404 ) ); } $role = $request->has_param( 'role' ) ? $request['role'] : $membership['role']; $status = $request->has_param( 'status' ) ? $request['status'] : $membership['status']; if ( 'invited' === $membership['status'] && 'active' === $status ) { return new WP_Error( 'spacebay_invite_pending', __( 'Invited people join when they accept the invitation.', 'spacebay' ), array( 'status' => 400 ) ); } // Blocking an invited person withdraws the invitation. Keeping a "blocked" // row would let a space admin unblock it to "active" and force them in. if ( 'invited' === $membership['status'] && 'blocked' === $status ) { Membership::remove( $space['id'], $target_id ); return rest_ensure_response( $this->membership_response( $space['id'], $target_id ) ); } if ( $role !== $membership['role'] && 'active' !== $status ) { return new WP_Error( 'spacebay_role_inactive', __( 'Only active members can get a new role.', 'spacebay' ), array( 'status' => 400 ) ); } $loses_admin = 'admin' === $membership['role'] && 'active' === $membership['status'] && ( 'admin' !== $role || 'active' !== $status ); if ( $loses_admin && Membership::count_admins( $space['id'] ) <= 1 ) { return new WP_Error( 'spacebay_last_admin', __( 'A space needs at least one admin. Make someone else an admin first.', 'spacebay' ), array( 'status' => 400 ) ); } Membership::set( $space['id'], $target_id, $role, $status ); return rest_ensure_response( $this->membership_response( $space['id'], $target_id ) ); } /** * Removes a member, declines a request or withdraws an invitation. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { $space = $this->manageable_space( $request['id'] ); if ( is_wp_error( $space ) ) { return $space; } $target_id = absint( $request['user_id'] ); $membership = Membership::get( $space['id'], $target_id ); if ( ! $membership ) { return new WP_Error( 'spacebay_not_member', __( 'This person is not in the space.', 'spacebay' ), array( 'status' => 404 ) ); } if ( 'admin' === $membership['role'] && 'active' === $membership['status'] && Membership::count_admins( $space['id'] ) <= 1 ) { return new WP_Error( 'spacebay_last_admin', __( 'A space needs at least one admin. Make someone else an admin first.', 'spacebay' ), array( 'status' => 400 ) ); } Membership::remove( $space['id'], $target_id ); return rest_ensure_response( array( 'removed' => true, 'user_id' => $target_id, ) ); } /** * Returns a space the current user manages, or an error. * * @param int $id Space ID. * @return array|WP_Error */ private function manageable_space( $id ) { $space = $this->visible_space( $id ); if ( is_wp_error( $space ) ) { return $space; } if ( ! SpaceAccess::can_manage( $space, get_current_user_id() ) ) { return $this->forbidden(); } return $space; } /** * Formats one membership. * * @param int $space_id Space ID. * @param int $user_id User ID. * @return array */ private function membership_response( $space_id, $user_id ) { $membership = Membership::get( $space_id, $user_id ); return array_merge( MemberDirectory::format( get_userdata( $user_id ) ), array( 'space_role' => $membership ? $membership['role'] : null, 'space_status' => $membership ? $membership['status'] : 'none', ) ); } /** * Number of excluded users who are members of the space with this status, * so the total matches what is listed. * * @param int $space_id Space ID. * @param string $status Membership status listed. * @param int[] $excluded Excluded user IDs. * @return int */ private static function excluded_in_space( $space_id, $status, array $excluded ) { $count = 0; foreach ( array_slice( $excluded, 0, 500 ) as $excluded_id ) { $membership = Membership::get( $space_id, $excluded_id ); if ( $membership && $status === $membership['status'] ) { ++$count; } } return $count; } }