HEX
Server: Apache/2.4.66 (Debian)
System: Linux 6dfabc3b2241 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64
User: (1000)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/wp-graphql/src/Data/Connection/UserConnectionResolver.php
<?php

namespace WPGraphQL\Data\Connection;

use GraphQL\Error\UserError;
use WPGraphQL\Utils\Utils;

/**
 * Class UserConnectionResolver
 *
 * @package WPGraphQL\Data\Connection
 * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_User_Query>
 */
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<string,mixed> $args The query "where" args
	 *
	 * @return array<string,mixed>
	 * @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<string,mixed>                  $query_args The mapped query args
		 * @param array<string,mixed>                  $args       The query "where" args
		 * @param mixed                                $source     The query results of the query calling this relation
		 * @param array<string,mixed>                  $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
		 *
		 * @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 ) );
	}
}