• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql / #887

16 Jan 2025 10:08PM UTC coverage: 83.189% (-0.8%) from 83.968%
#887

push

php-coveralls

web-flow
Merge pull request #3272 from wp-graphql/release/v1.30.0

release: v1.30.0

473 of 718 new or added lines in 23 files covered. (65.88%)

2 existing lines in 2 files now uncovered.

12995 of 15621 relevant lines covered (83.19%)

298.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.5
/src/Data/NodeResolver.php
1
<?php
2

3
namespace WPGraphQL\Data;
4

5
use GraphQL\Deferred;
6
use GraphQL\Error\UserError;
7
use WPGraphQL\AppContext;
8
use WPGraphQL\Router;
9
use WPGraphQL\Utils\Utils;
10
use WP_Post;
11

12
class NodeResolver {
13

14
        /**
15
         * @var \WP
16
         */
17
        protected $wp;
18

19
        /**
20
         * @var \WPGraphQL\AppContext
21
         */
22
        protected $context;
23

24
        /**
25
         * @var string
26
         */
27
        protected $route;
28

29
        /**
30
         * NodeResolver constructor.
31
         *
32
         * @param \WPGraphQL\AppContext $context
33
         *
34
         * @return void
35
         */
36
        public function __construct( AppContext $context ) {
37
                global $wp;
749✔
38
                $this->wp               = $wp;
749✔
39
                $this->route            = Router::$route . '/?$';
749✔
40
                $this->wp->matched_rule = $this->route;
749✔
41
                $this->context          = $context;
749✔
42
        }
43

44
        /**
45
         * Given a Post object, validates it before returning it.
46
         *
47
         * @param \WP_Post $post
48
         *
49
         * @return \WP_Post|null
50
         */
51
        public function validate_post( WP_Post $post ) {
52
                if ( isset( $this->wp->query_vars['post_type'] ) && ( $post->post_type !== $this->wp->query_vars['post_type'] ) ) {
54✔
53
                        return null;
3✔
54
                }
55

56
                if ( ! $this->is_valid_node_type( 'ContentNode' ) ) {
53✔
57
                        return null;
2✔
58
                }
59

60
                if ( empty( $this->wp->query_vars['uri'] ) ) {
51✔
UNCOV
61
                        return $post;
×
62
                }
63

64
                // if the uri doesn't have the post's urlencoded name or ID in it, we must've found something we didn't expect
65
                // so we will return null
66
                if ( false === strpos( $this->wp->query_vars['uri'], (string) $post->ID ) && false === strpos( $this->wp->query_vars['uri'], urldecode( sanitize_title( $post->post_name ) ) ) ) {
51✔
67
                        return null;
1✔
68
                }
69

70
                return $post;
51✔
71
        }
72

73
        /**
74
         * Given a Term object, validates it before returning it.
75
         *
76
         * @param \WP_Term $term
77
         *
78
         * @return \WP_Term|null
79
         */
80
        public function validate_term( \WP_Term $term ) {
81
                if ( ! $this->is_valid_node_type( 'TermNode' ) ) {
19✔
82
                        return null;
2✔
83
                }
84

85
                if ( isset( $this->wp->query_vars['taxonomy'] ) && $term->taxonomy !== $this->wp->query_vars['taxonomy'] ) {
17✔
86
                        return null;
1✔
87
                }
88

89
                return $term;
16✔
90
        }
91

92
        /**
93
         * Given the URI of a resource, this method attempts to resolve it and return the
94
         * appropriate related object
95
         *
96
         * @param string                     $uri              The path to be used as an identifier for the resource.
97
         * @param array<string,mixed>|string $extra_query_vars Any extra query vars to consider
98
         *
99
         * @return mixed
100
         * @throws \GraphQL\Error\UserError If the query class does not exist.
101
         */
102
        public function resolve_uri( string $uri, $extra_query_vars = '' ) {
103

104
                /**
105
                 * When this filter return anything other than null, it will be used as a resolved node
106
                 * and the execution will be skipped.
107
                 *
108
                 * This is to be used in extensions to resolve their own nodes which might not use
109
                 * WordPress permalink structure.
110
                 *
111
                 * @param mixed|null $node The node, defaults to nothing.
112
                 * @param string $uri The uri being searched.
113
                 * @param \WPGraphQL\AppContext $content The app context.
114
                 * @param \WP $wp WP object.
115
                 * @param array<string,mixed>|string $extra_query_vars Any extra query vars to consider.
116
                 */
117
                $node = apply_filters( 'graphql_pre_resolve_uri', null, $uri, $this->context, $this->wp, $extra_query_vars );
83✔
118

119
                if ( ! empty( $node ) ) {
83✔
120
                        return $node;
×
121
                }
122

123
                /**
124
                 * Comments are embedded as a #comment-{$id} in the post's content.
125
                 *
126
                 * If the URI is for a comment, we can resolve it now.
127
                 */
128
                $comment_id = $this->maybe_parse_comment_uri( $uri );
83✔
129
                if ( null !== $comment_id ) {
83✔
130
                        return $this->context->get_loader( 'comment' )->load_deferred( $comment_id );
1✔
131
                }
132

133
                /**
134
                 * Try to resolve the URI with WP_Query.
135
                 *
136
                 * This is the way WordPress native permalinks are resolved.
137
                 *
138
                 * @see \WP::main()
139
                 */
140

141
                // Parse the URI and sets the $wp->query_vars property.
142
                $uri = $this->parse_request( $uri, $extra_query_vars );
83✔
143

144
                /**
145
                 * If the URI is '/', we can resolve it now.
146
                 *
147
                 * We don't rely on $this->parse_request(), since the home page doesn't get a rewrite rule.
148
                 */
149
                if ( '/' === $uri ) {
83✔
150
                        return $this->resolve_home_page();
2✔
151
                }
152

153
                /**
154
                 * Filter the query class used to resolve the URI. By default this is WP_Query.
155
                 *
156
                 * This can be used by Extensions which use a different query class to resolve data.
157
                 *
158
                 * @param class-string               $query_class The query class used to resolve the URI. Defaults to WP_Query.
159
                 * @param ?string                    $uri The uri being searched.
160
                 * @param \WPGraphQL\AppContext      $content The app context.
161
                 * @param \WP                        $wp WP object.
162
                 * @param array<string,mixed>|string $extra_query_vars Any extra query vars to consider.
163
                 */
164
                $query_class = apply_filters( 'graphql_resolve_uri_query_class', 'WP_Query', $uri, $this->context, $this->wp, $extra_query_vars );
81✔
165

166
                if ( ! class_exists( $query_class ) ) {
81✔
167
                        throw new UserError(
×
168
                                esc_html(
×
169
                                        sprintf(
×
170
                                        /* translators: %s: The query class used to resolve the URI */
171
                                                __( 'The query class %s used to resolve the URI does not exist.', 'wp-graphql' ),
×
172
                                                $query_class
×
173
                                        )
×
174
                                )
×
175
                        );
×
176
                }
177

178
                $query_vars = $this->wp->query_vars;
81✔
179

180
                /** @var \WP_Query $query */
181
                $query = new $query_class( $query_vars );
81✔
182

183
                // is the query is an archive
184
                if ( isset( $query->posts[0] ) && $query->posts[0] instanceof WP_Post && ! $query->is_archive() ) {
81✔
185
                        $queried_object = $query->posts[0];
53✔
186
                } else {
187
                        $queried_object = $query->get_queried_object();
37✔
188
                }
189

190
                /**
191
                 * When this filter return anything other than null, it will be used as a resolved node
192
                 * and the execution will be skipped.
193
                 *
194
                 * This is to be used in extensions to resolve their own nodes which might not use
195
                 * WordPress permalink structure.
196
                 *
197
                 * It differs from 'graphql_pre_resolve_uri' in that it has been called after the query has been run using the query vars.
198
                 *
199
                 * @param mixed|null                                    $node             The node, defaults to nothing.
200
                 * @param ?string                                       $uri              The uri being searched.
201
                 * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|null $queried_object   The queried object, if WP_Query returns one.
202
                 * @param \WP_Query                                     $query            The query object.
203
                 * @param \WPGraphQL\AppContext                         $content          The app context.
204
                 * @param \WP                                           $wp               WP object.
205
                 * @param array<string,mixed>|string                    $extra_query_vars Any extra query vars to consider.
206
                 */
207
                $node = apply_filters( 'graphql_resolve_uri', null, $uri, $queried_object, $query, $this->context, $this->wp, $extra_query_vars );
81✔
208

209
                if ( ! empty( $node ) ) {
81✔
210
                        return $node;
×
211
                }
212

213
                // Resolve Post Objects.
214
                if ( $queried_object instanceof WP_Post ) {
81✔
215

216
                        // If Page for Posts is set, we need to return the Page archive, not the page.
217
                        if ( $query->is_posts_page ) {
55✔
218
                                // If were intentionally querying for a something other than a ContentType, we need to return null instead of the archive.
219
                                if ( ! $this->is_valid_node_type( 'ContentType' ) ) {
3✔
220
                                        return null;
1✔
221
                                }
222

223
                                $post_type_object = get_post_type_object( 'post' );
2✔
224

225
                                if ( ! $post_type_object ) {
2✔
226
                                        return null;
×
227
                                }
228

229
                                return ! empty( $post_type_object->name ) ? $this->context->get_loader( 'post_type' )->load_deferred( $post_type_object->name ) : null;
2✔
230
                        }
231

232
                        // Validate the post before returning it.
233
                        if ( ! $this->validate_post( $queried_object ) ) {
54✔
234
                                return null;
6✔
235
                        }
236

237
                        if ( empty( $extra_query_vars ) && isset( $this->wp->query_vars['error'] ) && '404' === $this->wp->query_vars['error'] ) {
51✔
238
                                return null;
1✔
239
                        }
240

241
                        $post_id = $queried_object->ID;
51✔
242

243
                        $as_preview = false;
51✔
244

245
                        // if asPreview isn't passed explicitly as an argument on a node,
246
                        // attempt to fill the value from the $query_vars passed on the URI as a query param
247
                        if ( is_array( $extra_query_vars ) && array_key_exists( 'asPreview', $extra_query_vars ) && null === $extra_query_vars['asPreview'] && isset( $query_vars['preview'] ) ) {
51✔
248
                                // note, the "preview" arg comes through as a string, not a boolean so we need to check 'true' as a string
249
                                $as_preview = 'true' === $query_vars['preview'];
1✔
250
                        }
251

252
                        $as_preview = isset( $extra_query_vars['asPreview'] ) && true === $extra_query_vars['asPreview'] ? true : $as_preview;
51✔
253

254
                        if ( true === $as_preview ) {
51✔
255
                                $post_id = Utils::get_post_preview_id( $post_id );
7✔
256
                        }
257

258
                        return ! empty( $post_id ) ? $this->context->get_loader( 'post' )->load_deferred( $post_id ) : null;
51✔
259
                }
260

261
                // Resolve Terms.
262
                if ( $queried_object instanceof \WP_Term ) {
33✔
263
                        // Validate the term before returning it.
264
                        if ( ! $this->validate_term( $queried_object ) ) {
19✔
265
                                return null;
3✔
266
                        }
267

268
                        return ! empty( $queried_object->term_id ) ? $this->context->get_loader( 'term' )->load_deferred( $queried_object->term_id ) : null;
16✔
269
                }
270

271
                // Resolve Post Types.
272
                if ( $queried_object instanceof \WP_Post_Type ) {
17✔
273

274
                        // Bail if we're explicitly requesting a different GraphQL type.
275
                        if ( ! $this->is_valid_node_type( 'ContentType' ) ) {
2✔
276
                                return null;
×
277
                        }
278

279
                        return ! empty( $queried_object->name ) ? $this->context->get_loader( 'post_type' )->load_deferred( $queried_object->name ) : null;
2✔
280
                }
281

282
                // Resolve Users
283
                if ( $queried_object instanceof \WP_User ) {
15✔
284
                        // Bail if we're explicitly requesting a different GraphQL type.
285
                        if ( ! $this->is_valid_node_type( 'User' ) ) {
5✔
286
                                return null;
1✔
287
                        }
288

289
                        return ! empty( $queried_object->ID ) ? $this->context->get_loader( 'user' )->load_deferred( $queried_object->ID ) : null;
4✔
290
                }
291

292
                /**
293
                 * This filter provides a fallback for resolving nodes that were unable to be resolved by NodeResolver::resolve_uri.
294
                 *
295
                 * This can be used by Extensions to resolve edge cases that are not handled by the core NodeResolver.
296
                 *
297
                 * @param mixed|null                                    $node             The node, defaults to nothing.
298
                 * @param ?string                                       $uri              The uri being searched.
299
                 * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|null $queried_object   The queried object, if WP_Query returns one.
300
                 * @param \WP_Query                                     $query            The query object.
301
                 * @param \WPGraphQL\AppContext                         $content          The app context.
302
                 * @param \WP                                           $wp               WP object.
303
                 * @param array<string,mixed>|string                    $extra_query_vars Any extra query vars to consider.
304
                 */
305
                return apply_filters( 'graphql_post_resolve_uri', $node, $uri, $queried_object, $query, $this->context, $this->wp, $extra_query_vars );
11✔
306
        }
307

308
        /**
309
         * Parses a URL to produce an array of query variables.
310
         *
311
         * Mimics WP::parse_request()
312
         *
313
         * @param string                     $uri
314
         * @param array<string,mixed>|string $extra_query_vars
315
         *
316
         * @return string|null The parsed uri.
317
         */
318
        public function parse_request( string $uri, $extra_query_vars = '' ) {
319
                // Attempt to parse the provided URI.
320
                $parsed_url = wp_parse_url( $uri );
83✔
321

322
                if ( false === $parsed_url ) {
83✔
323
                        graphql_debug(
×
324
                                __( 'Cannot parse provided URI', 'wp-graphql' ),
×
325
                                [
×
326
                                        'uri' => $uri,
×
327
                                ]
×
328
                        );
×
329
                        return null;
×
330
                }
331

332
                // Bail if external URI.
333
                if ( isset( $parsed_url['host'] ) ) {
83✔
334
                        $site_url = wp_parse_url( site_url() );
27✔
335
                        $home_url = wp_parse_url( home_url() );
27✔
336

337
                        /**
338
                         * @var array<string,mixed> $home_url
339
                         * @var array<string,mixed> $site_url
340
                         */
341
                        if ( ! in_array(
27✔
342
                                $parsed_url['host'],
27✔
343
                                [
27✔
344
                                        $site_url['host'],
27✔
345
                                        $home_url['host'],
27✔
346
                                ],
27✔
347
                                true
27✔
348
                        ) ) {
27✔
349
                                graphql_debug(
2✔
350
                                        __( 'Cannot return a resource for an external URI', 'wp-graphql' ),
2✔
351
                                        [
2✔
352
                                                'uri' => $uri,
2✔
353
                                        ]
2✔
354
                                );
2✔
355
                                return null;
2✔
356
                        }
357
                }
358

359
                if ( isset( $parsed_url['query'] ) && ( empty( $parsed_url['path'] ) || '/' === $parsed_url['path'] ) ) {
81✔
360
                        $uri = $parsed_url['query'];
16✔
361
                } elseif ( isset( $parsed_url['path'] ) ) {
75✔
362
                        $uri = $parsed_url['path'];
75✔
363
                }
364

365
                /**
366
                 * Follows pattern from WP::parse_request()
367
                 *
368
                 * @see https://github.com/WordPress/wordpress-develop/blob/6.0.2/src/wp-includes/class-wp.php#L135
369
                 */
370
                global $wp_rewrite;
81✔
371

372
                $this->wp->query_vars = [];
81✔
373
                $post_type_query_vars = [];
81✔
374

375
                if ( is_array( $extra_query_vars ) ) {
81✔
376
                        $this->wp->query_vars = &$extra_query_vars;
49✔
377
                } elseif ( ! empty( $extra_query_vars ) ) {
33✔
378
                        parse_str( $extra_query_vars, $this->wp->extra_query_vars );
×
379
                }
380

381
                // Set uri to Query vars.
382
                $this->wp->query_vars['uri'] = $uri;
81✔
383

384
                // Process PATH_INFO, REQUEST_URI, and 404 for permalinks.
385

386
                // Fetch the rewrite rules.
387
                $rewrite = $wp_rewrite->wp_rewrite_rules();
81✔
388
                if ( ! empty( $rewrite ) ) {
81✔
389
                        // If we match a rewrite rule, this will be cleared.
390
                        $error                   = '404';
81✔
391
                        $this->wp->did_permalink = true;
81✔
392

393
                        $pathinfo         = ! empty( $uri ) ? $uri : '';
81✔
394
                        list( $pathinfo ) = explode( '?', $pathinfo );
81✔
395
                        $pathinfo         = str_replace( '%', '%25', $pathinfo );
81✔
396

397
                        list( $req_uri ) = explode( '?', $pathinfo );
81✔
398
                        $home_path       = parse_url( home_url(), PHP_URL_PATH ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
81✔
399
                        $home_path_regex = '';
81✔
400
                        if ( is_string( $home_path ) && '' !== $home_path ) {
81✔
401
                                $home_path       = trim( $home_path, '/' );
×
402
                                $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) );
×
403
                        }
404

405
                        /*
406
                         * Trim path info from the end and the leading home path from the front.
407
                         * For path info requests, this leaves us with the requesting filename, if any.
408
                         * For 404 requests, this leaves us with the requested permalink.
409
                         */
410
                        $query    = '';
81✔
411
                        $matches  = null;
81✔
412
                        $req_uri  = str_replace( $pathinfo, '', $req_uri );
81✔
413
                        $req_uri  = trim( $req_uri, '/' );
81✔
414
                        $pathinfo = trim( $pathinfo, '/' );
81✔
415

416
                        if ( ! empty( $home_path_regex ) ) {
81✔
417
                                $req_uri  = preg_replace( $home_path_regex, '', $req_uri );
×
418
                                $req_uri  = trim( $req_uri, '/' ); // @phpstan-ignore-line
×
419
                                $pathinfo = preg_replace( $home_path_regex, '', $pathinfo );
×
420
                                $pathinfo = trim( $pathinfo, '/' ); // @phpstan-ignore-line
×
421
                        }
422

423
                        // The requested permalink is in $pathinfo for path info requests and
424
                        // $req_uri for other requests.
425
                        if ( ! empty( $pathinfo ) && ! preg_match( '|^.*' . $wp_rewrite->index . '$|', $pathinfo ) ) {
81✔
426
                                $requested_path = $pathinfo;
79✔
427
                        } else {
428
                                // If the request uri is the index, blank it out so that we don't try to match it against a rule.
429
                                if ( $req_uri === $wp_rewrite->index ) {
5✔
430
                                        $req_uri = '';
×
431
                                }
432
                                $requested_path = $req_uri;
5✔
433
                        }
434
                        $requested_file = $req_uri;
81✔
435

436
                        $this->wp->request = $requested_path;
81✔
437

438
                        // Look for matches.
439
                        $request_match = $requested_path;
81✔
440
                        if ( empty( $request_match ) ) {
81✔
441
                                // An empty request could only match against ^$ regex
442
                                if ( isset( $rewrite['$'] ) ) {
5✔
443
                                        $this->wp->matched_rule = '$';
×
444
                                        $query                  = $rewrite['$'];
×
445
                                        $matches                = [ '' ];
×
446
                                }
447
                        } else {
448
                                foreach ( (array) $rewrite as $match => $query ) {
79✔
449
                                        // If the requested file is the anchor of the match, prepend it to the path info.
450
                                        if ( ! empty( $requested_file ) && strpos( $match, $requested_file ) === 0 && $requested_file !== $requested_path ) {
79✔
451
                                                $request_match = $requested_file . '/' . $requested_path;
×
452
                                        }
453

454
                                        if (
455
                                                preg_match( "#^$match#", $request_match, $matches ) ||
79✔
456
                                                preg_match( "#^$match#", urldecode( $request_match ), $matches )
79✔
457
                                        ) {
458
                                                if ( $wp_rewrite->use_verbose_page_rules && preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch ) ) {
79✔
459
                                                        // This is a verbose page match, let's check to be sure about it.
460
                                                        $page = get_page_by_path( $matches[ $varmatch[1] ] ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_page_by_path_get_page_by_path
6✔
461
                                                        if ( ! $page ) {
6✔
462
                                                                continue;
5✔
463
                                                        }
464

465
                                                        $post_status_obj = get_post_status_object( $page->post_status );
2✔
466
                                                        if (
467
                                                                ( ! isset( $post_status_obj->public ) || ! $post_status_obj->public ) &&
2✔
468
                                                                ( ! isset( $post_status_obj->protected ) || ! $post_status_obj->protected ) &&
2✔
469
                                                                ( ! isset( $post_status_obj->private ) || ! $post_status_obj->private ) &&
2✔
470
                                                                ( ! isset( $post_status_obj->exclude_from_search ) || $post_status_obj->exclude_from_search )
2✔
471
                                                        ) {
472
                                                                continue;
×
473
                                                        }
474
                                                }
475

476
                                                // Got a match.
477
                                                $this->wp->matched_rule = $match;
76✔
478
                                                break;
76✔
479
                                        }
480
                                }
481
                        }
482

483
                        if ( ! empty( $this->wp->matched_rule ) && $this->wp->matched_rule !== $this->route ) {
81✔
484
                                // Trim the query of everything up to the '?'.
485
                                $query = preg_replace( '!^.+\?!', '', $query );
76✔
486

487
                                // Substitute the substring matches into the query.
488
                                $query = addslashes( \WP_MatchesMapRegex::apply( $query, $matches ) ); // @phpstan-ignore-line
76✔
489

490
                                $this->wp->matched_query = $query;
76✔
491

492
                                // Parse the query.
493
                                parse_str( $query, $perma_query_vars );
76✔
494

495
                                // If we're processing a 404 request, clear the error var since we found something.
496
                                // @phpstan-ignore-next-line
497
                                if ( '404' == $error ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
76✔
498
                                        unset( $error );
76✔
499
                                }
500
                        }
501
                }
502

503
                /**
504
                 * Filters the query variables allowed before processing.
505
                 *
506
                 * Allows (publicly allowed) query vars to be added, removed, or changed prior
507
                 * to executing the query. Needed to allow custom rewrite rules using your own arguments
508
                 * to work, or any other custom query variables you want to be publicly available.
509
                 *
510
                 * @since 1.5.0
511
                 *
512
                 * @param string[] $public_query_vars The array of allowed query variable names.
513
                 */
514
                $this->wp->public_query_vars = apply_filters( 'query_vars', $this->wp->public_query_vars );
81✔
515

516
                foreach ( get_post_types( [ 'show_in_graphql' => true ], 'objects' )  as $post_type => $t ) {
81✔
517
                        /** @var \WP_Post_Type $t */
518
                        if ( $t->query_var ) {
81✔
519
                                $post_type_query_vars[ $t->query_var ] = $post_type;
80✔
520
                        }
521
                }
522

523
                foreach ( $this->wp->public_query_vars as $wpvar ) {
81✔
524
                        $parsed_query = [];
81✔
525
                        if ( isset( $parsed_url['query'] ) ) {
81✔
526
                                parse_str( $parsed_url['query'], $parsed_query );
16✔
527
                        }
528

529
                        if ( isset( $this->wp->extra_query_vars[ $wpvar ] ) ) {
81✔
530
                                $this->wp->query_vars[ $wpvar ] = $this->wp->extra_query_vars[ $wpvar ];
×
531
                        } elseif ( isset( $_GET[ $wpvar ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
81✔
532
                                $this->wp->query_vars[ $wpvar ] = $_GET[ $wpvar ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
×
533
                        } elseif ( isset( $perma_query_vars[ $wpvar ] ) ) {
81✔
534
                                $this->wp->query_vars[ $wpvar ] = $perma_query_vars[ $wpvar ];
76✔
535
                        } elseif ( isset( $parsed_query[ $wpvar ] ) ) {
81✔
536
                                $this->wp->query_vars[ $wpvar ] = $parsed_query[ $wpvar ];
16✔
537
                        }
538

539
                        if ( ! empty( $this->wp->query_vars[ $wpvar ] ) ) {
81✔
540
                                if ( ! is_array( $this->wp->query_vars[ $wpvar ] ) ) {
78✔
541
                                        $this->wp->query_vars[ $wpvar ] = (string) $this->wp->query_vars[ $wpvar ];
78✔
542
                                } else {
543
                                        foreach ( $this->wp->query_vars[ $wpvar ] as $vkey => $v ) {
×
544
                                                if ( is_scalar( $v ) ) {
×
545
                                                        $this->wp->query_vars[ $wpvar ][ $vkey ] = (string) $v;
×
546
                                                }
547
                                        }
548
                                }
549

550
                                if ( isset( $post_type_query_vars[ $wpvar ] ) ) {
78✔
551
                                        $this->wp->query_vars['post_type'] = $post_type_query_vars[ $wpvar ];
7✔
552
                                        $this->wp->query_vars['name']      = $this->wp->query_vars[ $wpvar ];
7✔
553
                                }
554
                        }
555
                }
556

557
                // Convert urldecoded spaces back into '+'.
558
                foreach ( get_taxonomies( [ 'show_in_graphql' => true ], 'objects' ) as $t ) {
81✔
559
                        if ( $t->query_var && isset( $this->wp->query_vars[ $t->query_var ] ) ) {
81✔
560
                                $this->wp->query_vars[ $t->query_var ] = str_replace( ' ', '+', $this->wp->query_vars[ $t->query_var ] );
19✔
561
                        }
562
                }
563

564
                // Limit publicly queried post_types to those that are publicly_queryable
565
                if ( isset( $this->wp->query_vars['post_type'] ) ) {
81✔
566
                        $queryable_post_types = get_post_types( [ 'show_in_graphql' => true ] );
43✔
567
                        if ( ! is_array( $this->wp->query_vars['post_type'] ) ) {
43✔
568
                                if ( ! in_array( $this->wp->query_vars['post_type'], $queryable_post_types, true ) ) {
43✔
569
                                        unset( $this->wp->query_vars['post_type'] );
×
570
                                }
571
                        } else {
572
                                $this->wp->query_vars['post_type'] = array_intersect( $this->wp->query_vars['post_type'], $queryable_post_types );
×
573
                        }
574
                }
575

576
                // Resolve conflicts between posts with numeric slugs and date archive queries.
577
                $this->wp->query_vars = wp_resolve_numeric_slug_conflicts( $this->wp->query_vars );
81✔
578

579
                foreach ( (array) $this->wp->private_query_vars as $var ) {
81✔
580
                        if ( isset( $this->wp->extra_query_vars[ $var ] ) ) {
81✔
581
                                $this->wp->query_vars[ $var ] = $this->wp->extra_query_vars[ $var ];
×
582
                        }
583
                }
584

585
                if ( isset( $error ) ) {
81✔
586
                        $this->wp->query_vars['error'] = $error;
10✔
587
                }
588

589
                // if the parsed url is ONLY a query, unset the pagename query var
590
                if ( isset( $this->wp->query_vars['pagename'], $parsed_url['query'] ) && ( $parsed_url['query'] === $this->wp->query_vars['pagename'] ) ) {
81✔
591
                        unset( $this->wp->query_vars['pagename'] );
16✔
592
                }
593

594
                /**
595
                 * Filters the array of parsed query variables.
596
                 *
597
                 * @param array<string,mixed> $query_vars The array of requested query variables.
598
                 *
599
                 * @since 2.1.0
600
                 */
601
                $this->wp->query_vars = apply_filters( 'request', $this->wp->query_vars );
81✔
602

603
                // We don't need the GraphQL args anymore.
604
                unset( $this->wp->query_vars['graphql'] );
81✔
605

606
                do_action_ref_array( 'parse_request', [ &$this->wp ] );
81✔
607

608
                return $uri;
81✔
609
        }
610

611
        /**
612
         * Checks if the node type is set in the query vars and, if so, whether it matches the node type.
613
         *
614
         * @param string $node_type The node type to check.
615
         */
616
        protected function is_valid_node_type( string $node_type ): bool {
617
                return ! isset( $this->wp->query_vars['nodeType'] ) || $this->wp->query_vars['nodeType'] === $node_type;
78✔
618
        }
619

620
        /**
621
         * Resolves the home page.
622
         *
623
         * If the homepage is a static page, return the page, otherwise we return the Posts `ContentType`.
624
         *
625
         * @todo Replace `ContentType` with an `Archive` type.
626
         */
627
        protected function resolve_home_page(): ?Deferred {
628
                $page_id       = get_option( 'page_on_front', 0 );
2✔
629
                $show_on_front = get_option( 'show_on_front', 'posts' );
2✔
630

631
                // If the homepage is a static page, return the page.
632
                if ( 'page' === $show_on_front && ! empty( $page_id ) ) {
2✔
633
                        $page = get_post( $page_id );
2✔
634

635
                        if ( empty( $page ) ) {
2✔
636
                                return null;
×
637
                        }
638

639
                        return $this->context->get_loader( 'post' )->load_deferred( $page->ID );
2✔
640
                }
641

642
                // If the homepage is set to latest posts, we need to make sure not to resolve it when when for other types.
643
                if ( ! $this->is_valid_node_type( 'ContentType' ) ) {
2✔
644
                        return null;
1✔
645
                }
646

647
                // We dont have an 'Archive' type, so we resolve to the ContentType.
648
                return $this->context->get_loader( 'post_type' )->load_deferred( 'post' );
1✔
649
        }
650

651
        /**
652
         * Checks if the URI is a comment URI and, if so, returns the comment ID.
653
         *
654
         * @param string $uri The URI to check.
655
         */
656
        protected function maybe_parse_comment_uri( string $uri ): ?int {
657
                $comment_match = [];
83✔
658
                // look for a #comment-{$id} anywhere in the uri.
659
                if ( preg_match( '/#comment-(\d+)/', $uri, $comment_match ) ) {
83✔
660
                        $comment_id = absint( $comment_match[1] );
1✔
661

662
                        return ! empty( $comment_id ) ? $comment_id : null;
1✔
663
                }
664

665
                return null;
83✔
666
        }
667
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc