*/ class UserConnectionResolver extends AbstractConnectionResolver { /** * {@inheritDoc} */ protected function loader_name(): string { return 'user'; } /** * {@inheritDoc} * * @throws \Exception */ protected function prepare_query_args( array $args ): array { $query_args = []; /** * Prepare for later use */ $last = ! empty( $args['last'] ) ? $args['last'] : null; /** * Set the $query_args based on various defaults and primary input $args */ $query_args['count_total'] = false; /** * Pass the graphql $args to the WP_Query */ $query_args['graphql_args'] = $args; /** * Set the graphql_cursor_compare to determine what direction the * query should be paginated */ $query_args['graphql_cursor_compare'] = ( ! empty( $last ) ) ? '>' : '<'; $query_args['graphql_after_cursor'] = $this->get_after_offset(); $query_args['graphql_before_cursor'] = $this->get_before_offset(); /** * Set the number, ensuring it doesn't exceed the amount set as the $max_query_amount * * We query one extra than what is being asked for so that we can determine if there is a next * page. */ $query_args['number'] = $this->get_query_amount() + 1; /** * Take any of the input $args (under the "where" input) that were part of the GraphQL query and map and * sanitize their GraphQL input to apply to the WP_Query */ $input_fields = []; if ( ! empty( $args['where'] ) ) { $input_fields = $this->sanitize_input_fields( $args['where'] ); } /** * Merge the default $query_args with the $args that were entered in the query. * * @since 0.0.5 */ if ( ! empty( $input_fields ) ) { $query_args = array_merge( $query_args, $input_fields ); } /** * Only query the IDs and let deferred resolution query the nodes */ $query_args['fields'] = 'ID'; /** * If the request is not authenticated, limit the query to users that have * published posts, as they're considered publicly facing users. */ if ( ! is_user_logged_in() && empty( $query_args['has_published_posts'] ) ) { $query_args['has_published_posts'] = true; } /** * If `has_published_posts` is set to `attachment`, throw a warning. * * @todo Remove this when the `hasPublishedPosts` enum type changes. * * @see https://github.com/wp-graphql/wp-graphql/issues/2963 */ if ( ! empty( $query_args['has_published_posts'] ) && 'attachment' === $query_args['has_published_posts'] ) { graphql_debug( __( 'The `hasPublishedPosts` where arg does not support the `ATTACHMENT` value, and will be removed from the possible enum values in a future release.', 'wp-graphql' ), [ 'operationName' => $this->context->operationName ?? '', 'query' => $this->context->query ?? '', 'variables' => $this->context->variables ?? '', ] ); } if ( ! empty( $query_args['search'] ) ) { $query_args['search'] = '*' . $query_args['search'] . '*'; $query_args['orderby'] = 'user_login'; $query_args['order'] = ! empty( $last ) ? 'DESC' : 'ASC'; } /** * Map the orderby inputArgs to the WP_User_Query */ if ( ! empty( $args['where']['orderby'] ) && is_array( $args['where']['orderby'] ) ) { foreach ( $args['where']['orderby'] as $orderby_input ) { /** * These orderby options should not include the order parameter. */ if ( in_array( $orderby_input['field'], [ 'login__in', 'nicename__in', ], true ) ) { $query_args['orderby'] = esc_sql( $orderby_input['field'] ); } elseif ( ! empty( $orderby_input['field'] ) ) { $order = $orderby_input['order']; if ( ! empty( $args['last'] ) ) { if ( 'ASC' === $order ) { $order = 'DESC'; } else { $order = 'ASC'; } } $query_args['orderby'] = esc_sql( $orderby_input['field'] ); $query_args['order'] = esc_sql( $order ); } } } /** * Convert meta_value_num to separate meta_value value field which our * graphql_wp_term_query_cursor_pagination_support knowns how to handle */ if ( isset( $query_args['orderby'] ) && 'meta_value_num' === $query_args['orderby'] ) { $query_args['orderby'] = [ 'meta_value' => empty( $query_args['order'] ) ? 'DESC' : $query_args['order'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ]; unset( $query_args['order'] ); $query_args['meta_type'] = 'NUMERIC'; } /** * If there's no orderby params in the inputArgs, set order based on the first/last argument */ if ( empty( $query_args['order'] ) ) { $query_args['order'] = ! empty( $last ) ? 'DESC' : 'ASC'; } return $query_args; } /** * {@inheritDoc} */ protected function query_class(): string { return \WP_User_Query::class; } /** * {@inheritDoc} */ public function get_ids_from_query() { /** * @todo This is for b/c. We can just use $this->get_query(). */ $queried = isset( $this->query ) ? $this->query : $this->get_query(); /** @var int[] $ids */ $ids = $queried->get_results(); // If we're going backwards, we need to reverse the array. $args = $this->get_args(); if ( ! empty( $args['last'] ) ) { $ids = array_reverse( $ids ); } return $ids; } /** * This sets up the "allowed" args, and translates the GraphQL-friendly keys to WP_User_Query * friendly keys. * * There's probably a cleaner/more dynamic way to approach this, but this was quick. I'd be * down to explore more dynamic ways to map this, but for now this gets the job done. * * @param array $args The query "where" args * * @return array * @throws \GraphQL\Error\UserError If the user does not have the "list_users" capability. * @since 0.0.5 */ protected function sanitize_input_fields( array $args ) { /** * Only users with the "list_users" capability can filter users by roles */ if ( ( ! empty( $args['roleIn'] ) || ! empty( $args['roleNotIn'] ) || ! empty( $args['role'] ) ) && ! current_user_can( 'list_users' ) ) { throw new UserError( esc_html__( 'Sorry, you are not allowed to filter users by role.', 'wp-graphql' ) ); } $arg_mapping = [ 'roleIn' => 'role__in', 'roleNotIn' => 'role__not_in', 'searchColumns' => 'search_columns', 'hasPublishedPosts' => 'has_published_posts', 'nicenameIn' => 'nicename__in', 'nicenameNotIn' => 'nicename__not_in', 'loginIn' => 'login__in', 'loginNotIn' => 'login__not_in', ]; /** * Map and sanitize the input args to the WP_User_Query compatible args */ $query_args = Utils::map_input( $args, $arg_mapping ); /** * Filter the input fields * * This allows plugins/themes to hook in and alter what $args should be allowed to be passed * from a GraphQL Query to the WP_User_Query * * @param array $query_args The mapped query args * @param array $args The query "where" args * @param mixed $source The query results of the query calling this relation * @param array $all_args Array of all the query args (not just the "where" args) * @param \WPGraphQL\AppContext $context The AppContext object * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object * * @hookGroup connections * @since 0.0.5 */ $query_args = apply_filters( 'graphql_map_input_fields_to_wp_user_query', $query_args, $args, $this->source, $this->get_args(), $this->context, $this->info ); return ! empty( $query_args ) && is_array( $query_args ) ? $query_args : []; } /** * {@inheritDoc} * * @param int $offset The ID of the node used as the offset in the cursor. */ public function is_valid_offset( $offset ) { return (bool) get_user_by( 'ID', absint( $offset ) ); } }