PluginProbe
WPGraphQL / trunk
WPGraphQL vtrunk
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / Data / Connection / UserConnectionResolver.php

UserConnectionResolver.php in WPGraphQL trunk, at src/Data/Connection/UserConnectionResolver.php

275 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPGraphQL\Data\Connection;
4
5 use GraphQL\Error\UserError;
6 use WPGraphQL\Utils\Utils;
7
8 /**
9 * Class UserConnectionResolver
10 *
11 * @package WPGraphQL\Data\Connection
12 * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_User_Query>
13 */
14 class UserConnectionResolver extends AbstractConnectionResolver {
15 /**
16 * {@inheritDoc}
17 */
18 protected function loader_name(): string {
19 return 'user';
20 }
21
22 /**
23 * {@inheritDoc}
24 *
25 * @throws \Exception
26 */
27 protected function prepare_query_args( array $args ): array {
28 $query_args = [];
29
30 /**
31 * Prepare for later use
32 */
33 $last = ! empty( $args['last'] ) ? $args['last'] : null;
34
35 /**
36 * Set the $query_args based on various defaults and primary input $args
37 */
38 $query_args['count_total'] = false;
39
40 /**
41 * Pass the graphql $args to the WP_Query
42 */
43 $query_args['graphql_args'] = $args;
44
45 /**
46 * Set the graphql_cursor_compare to determine what direction the
47 * query should be paginated
48 */
49 $query_args['graphql_cursor_compare'] = ( ! empty( $last ) ) ? '>' : '<';
50
51 $query_args['graphql_after_cursor'] = $this->get_after_offset();
52 $query_args['graphql_before_cursor'] = $this->get_before_offset();
53
54 /**
55 * Set the number, ensuring it doesn't exceed the amount set as the $max_query_amount
56 *
57 * We query one extra than what is being asked for so that we can determine if there is a next
58 * page.
59 */
60 $query_args['number'] = $this->get_query_amount() + 1;
61
62 /**
63 * Take any of the input $args (under the "where" input) that were part of the GraphQL query and map and
64 * sanitize their GraphQL input to apply to the WP_Query
65 */
66 $input_fields = [];
67 if ( ! empty( $args['where'] ) ) {
68 $input_fields = $this->sanitize_input_fields( $args['where'] );
69 }
70
71 /**
72 * Merge the default $query_args with the $args that were entered in the query.
73 *
74 * @since 0.0.5
75 */
76 if ( ! empty( $input_fields ) ) {
77 $query_args = array_merge( $query_args, $input_fields );
78 }
79
80 /**
81 * Only query the IDs and let deferred resolution query the nodes
82 */
83 $query_args['fields'] = 'ID';
84
85 /**
86 * If the request is not authenticated, limit the query to users that have
87 * published posts, as they're considered publicly facing users.
88 */
89 if ( ! is_user_logged_in() && empty( $query_args['has_published_posts'] ) ) {
90 $query_args['has_published_posts'] = true;
91 }
92
93 /**
94 * If `has_published_posts` is set to `attachment`, throw a warning.
95 *
96 * @todo Remove this when the `hasPublishedPosts` enum type changes.
97 *
98 * @see https://github.com/wp-graphql/wp-graphql/issues/2963
99 */
100 if ( ! empty( $query_args['has_published_posts'] ) && 'attachment' === $query_args['has_published_posts'] ) {
101 graphql_debug(
102 __( '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' ),
103 [
104 'operationName' => $this->context->operationName ?? '',
105 'query' => $this->context->query ?? '',
106 'variables' => $this->context->variables ?? '',
107 ]
108 );
109 }
110
111 if ( ! empty( $query_args['search'] ) ) {
112 $query_args['search'] = '*' . $query_args['search'] . '*';
113 $query_args['orderby'] = 'user_login';
114 $query_args['order'] = ! empty( $last ) ? 'DESC' : 'ASC';
115 }
116
117 /**
118 * Map the orderby inputArgs to the WP_User_Query
119 */
120 if ( ! empty( $args['where']['orderby'] ) && is_array( $args['where']['orderby'] ) ) {
121 foreach ( $args['where']['orderby'] as $orderby_input ) {
122 /**
123 * These orderby options should not include the order parameter.
124 */
125 if ( in_array(
126 $orderby_input['field'],
127 [
128 'login__in',
129 'nicename__in',
130 ],
131 true
132 ) ) {
133 $query_args['orderby'] = esc_sql( $orderby_input['field'] );
134 } elseif ( ! empty( $orderby_input['field'] ) ) {
135 $order = $orderby_input['order'];
136 if ( ! empty( $args['last'] ) ) {
137 if ( 'ASC' === $order ) {
138 $order = 'DESC';
139 } else {
140 $order = 'ASC';
141 }
142 }
143
144 $query_args['orderby'] = esc_sql( $orderby_input['field'] );
145 $query_args['order'] = esc_sql( $order );
146 }
147 }
148 }
149
150 /**
151 * Convert meta_value_num to separate meta_value value field which our
152 * graphql_wp_term_query_cursor_pagination_support knowns how to handle
153 */
154 if ( isset( $query_args['orderby'] ) && 'meta_value_num' === $query_args['orderby'] ) {
155 $query_args['orderby'] = [
156 'meta_value' => empty( $query_args['order'] ) ? 'DESC' : $query_args['order'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
157 ];
158 unset( $query_args['order'] );
159 $query_args['meta_type'] = 'NUMERIC';
160 }
161
162 /**
163 * If there's no orderby params in the inputArgs, set order based on the first/last argument
164 */
165 if ( empty( $query_args['order'] ) ) {
166 $query_args['order'] = ! empty( $last ) ? 'DESC' : 'ASC';
167 }
168
169 return $query_args;
170 }
171
172 /**
173 * {@inheritDoc}
174 */
175 protected function query_class(): string {
176 return \WP_User_Query::class;
177 }
178
179 /**
180 * {@inheritDoc}
181 */
182 public function get_ids_from_query() {
183 /**
184 * @todo This is for b/c. We can just use $this->get_query().
185 */
186 $queried = isset( $this->query ) ? $this->query : $this->get_query();
187
188 /** @var int[] $ids */
189 $ids = $queried->get_results();
190
191 // If we're going backwards, we need to reverse the array.
192 $args = $this->get_args();
193 if ( ! empty( $args['last'] ) ) {
194 $ids = array_reverse( $ids );
195 }
196
197 return $ids;
198 }
199
200 /**
201 * This sets up the "allowed" args, and translates the GraphQL-friendly keys to WP_User_Query
202 * friendly keys.
203 *
204 * There's probably a cleaner/more dynamic way to approach this, but this was quick. I'd be
205 * down to explore more dynamic ways to map this, but for now this gets the job done.
206 *
207 * @param array<string,mixed> $args The query "where" args
208 *
209 * @return array<string,mixed>
210 * @throws \GraphQL\Error\UserError If the user does not have the "list_users" capability.
211 * @since 0.0.5
212 */
213 protected function sanitize_input_fields( array $args ) {
214
215 /**
216 * Only users with the "list_users" capability can filter users by roles
217 */
218 if (
219 (
220 ! empty( $args['roleIn'] ) ||
221 ! empty( $args['roleNotIn'] ) ||
222 ! empty( $args['role'] )
223 ) &&
224 ! current_user_can( 'list_users' )
225 ) {
226 throw new UserError( esc_html__( 'Sorry, you are not allowed to filter users by role.', 'wp-graphql' ) );
227 }
228
229 $arg_mapping = [
230 'roleIn' => 'role__in',
231 'roleNotIn' => 'role__not_in',
232 'searchColumns' => 'search_columns',
233 'hasPublishedPosts' => 'has_published_posts',
234 'nicenameIn' => 'nicename__in',
235 'nicenameNotIn' => 'nicename__not_in',
236 'loginIn' => 'login__in',
237 'loginNotIn' => 'login__not_in',
238 ];
239
240 /**
241 * Map and sanitize the input args to the WP_User_Query compatible args
242 */
243 $query_args = Utils::map_input( $args, $arg_mapping );
244
245 /**
246 * Filter the input fields
247 *
248 * This allows plugins/themes to hook in and alter what $args should be allowed to be passed
249 * from a GraphQL Query to the WP_User_Query
250 *
251 * @param array<string,mixed> $query_args The mapped query args
252 * @param array<string,mixed> $args The query "where" args
253 * @param mixed $source The query results of the query calling this relation
254 * @param array<string,mixed> $all_args Array of all the query args (not just the "where" args)
255 * @param \WPGraphQL\AppContext $context The AppContext object
256 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
257 *
258 * @hookGroup connections
259 * @since 0.0.5
260 */
261 $query_args = apply_filters( 'graphql_map_input_fields_to_wp_user_query', $query_args, $args, $this->source, $this->get_args(), $this->context, $this->info );
262
263 return ! empty( $query_args ) && is_array( $query_args ) ? $query_args : [];
264 }
265
266 /**
267 * {@inheritDoc}
268 *
269 * @param int $offset The ID of the node used as the offset in the cursor.
270 */
271 public function is_valid_offset( $offset ) {
272 return (bool) get_user_by( 'ID', absint( $offset ) );
273 }
274 }
275