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

wp-graphql / wp-graphql / 17562196906

08 Sep 2025 07:44PM UTC coverage: 84.575% (+0.4%) from 84.17%
17562196906

push

github

web-flow
Merge pull request #3389 from wp-graphql/develop

release: next version 📦

238 of 308 new or added lines in 13 files covered. (77.27%)

6 existing lines in 6 files now uncovered.

15884 of 18781 relevant lines covered (84.57%)

261.69 hits per line

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

20.45
/src/Router.php
1
<?php
2

3
namespace WPGraphQL;
4

5
use GraphQL\Error\FormattedError;
6
use WP_User;
7

8
/**
9
 * Class Router
10
 * This sets up the /graphql endpoint
11
 *
12
 * @package WPGraphQL
13
 * @since   0.0.1
14
 *
15
 * phpcs:disable -- PHPStan annotation.
16
 * @phpstan-import-type SerializableError from \GraphQL\Executor\ExecutionResult
17
 * @phpstan-import-type SerializableResult from \GraphQL\Executor\ExecutionResult
18
 *
19
 * @phpstan-type WPGraphQLResult = SerializableResult|(\GraphQL\Executor\ExecutionResult|array<int,\GraphQL\Executor\ExecutionResult>)
20
 * phpcs:enable
21
 */
22
class Router {
23

24
        /**
25
         * Sets the route to use as the endpoint
26
         *
27
         * @var string $route
28
         */
29
        public static $route = 'graphql';
30

31
        /**
32
         * Holds the Global Post for later resetting
33
         *
34
         * @var string
35
         */
36
        protected static $global_post = '';
37

38
        /**
39
         * Set the default status code to 200.
40
         *
41
         * @var int
42
         */
43
        public static $http_status_code = 200;
44

45
        /**
46
         * @var ?\WPGraphQL\Request
47
         */
48
        protected static $request;
49

50
        /**
51
         * Initialize the WPGraphQL Router
52
         *
53
         * @return void
54
         * @throws \Exception
55
         */
56
        public function init() {
×
57
                self::$route = graphql_get_endpoint();
×
58

59
                /**
60
                 * Create the rewrite rule for the route
61
                 *
62
                 * @since 0.0.1
63
                 */
64
                add_action( 'init', [ $this, 'add_rewrite_rule' ], 10 );
×
65

66
                /**
67
                 * Add the query var for the route
68
                 *
69
                 * @since 0.0.1
70
                 */
71
                add_filter( 'query_vars', [ $this, 'add_query_var' ], 1, 1 );
×
72

73
                /**
74
                 * Redirects the route to the graphql processor
75
                 *
76
                 * @since 0.0.1
77
                 */
78
                add_action( 'parse_request', [ $this, 'resolve_http_request' ], 10 );
×
79

80
                /**
81
                 * Adds support for application passwords
82
                 */
83
                add_filter( 'application_password_is_api_request', [ $this, 'is_api_request' ] );
×
84
        }
85

86
        /**
87
         * Returns the GraphQL Request being executed
88
         */
89
        public static function get_request(): ?Request {
×
90
                return self::$request;
×
91
        }
92

93
        /**
94
         * Adds rewrite rule for the route endpoint
95
         *
96
         * @return void
97
         * @since  0.0.1
98
         * @uses   add_rewrite_rule()
99
         */
100
        public static function add_rewrite_rule() {
×
101
                add_rewrite_rule(
×
102
                        self::$route . '/?$',
×
103
                        'index.php?' . self::$route . '=true',
×
104
                        'top'
×
105
                );
×
106
        }
107

108
        /**
109
         * Determines whether the request is an API request to play nice with
110
         * application passwords and potential other WordPress core functionality
111
         * for APIs
112
         *
113
         * @param bool $is_api_request Whether the request is an API request
114
         *
115
         * @return bool
116
         */
117
        public function is_api_request( $is_api_request ) {
×
118
                return true === is_graphql_http_request() ? true : $is_api_request;
×
119
        }
120

121
        /**
122
         * Adds the query_var for the route
123
         *
124
         * @param string[] $query_vars The array of whitelisted query variables.
125
         *
126
         * @return string[]
127
         * @since  0.0.1
128
         */
129
        public static function add_query_var( $query_vars ) {
86✔
130
                $query_vars[] = self::$route;
86✔
131

132
                return $query_vars;
86✔
133
        }
134

135
        /**
136
         * Returns true when the current request is a GraphQL request coming from the HTTP
137
         *
138
         * NOTE: This will only indicate whether the GraphQL Request is an HTTP request. Many features
139
         * need to affect _all_ GraphQL requests, including internal requests using the `graphql()`
140
         * function, so be careful how you use this to check your conditions.
141
         *
142
         * @return bool
143
         */
144
        public static function is_graphql_http_request() {
777✔
145

146
                /**
147
                 * Filter whether the request is a GraphQL HTTP Request. Default is null, as the majority
148
                 * of WordPress requests are NOT GraphQL requests (at least today that's true 😆).
149
                 *
150
                 * If this filter returns anything other than null, the function will return now and skip the
151
                 * default checks.
152
                 *
153
                 * @param ?bool $is_graphql_http_request Whether the request is a GraphQL HTTP Request. Default false.
154
                 */
155
                $pre_is_graphql_http_request = apply_filters( 'graphql_pre_is_graphql_http_request', null );
777✔
156

157
                /**
158
                 * If the filter has been applied, return now before executing default checks
159
                 */
160
                if ( null !== $pre_is_graphql_http_request ) {
777✔
161
                        return (bool) $pre_is_graphql_http_request;
×
162
                }
163

164
                // Default is false
165
                $is_graphql_http_request = false;
777✔
166

167
                // Support wp-graphiql style request to /index.php?graphql.
168
                if ( isset( $_GET[ self::$route ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
777✔
169

170
                        $is_graphql_http_request = true;
1✔
171
                } elseif ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
776✔
172
                        // Check the server to determine if the GraphQL endpoint is being requested
173
                        $host = wp_unslash( $_SERVER['HTTP_HOST'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
776✔
174
                        $uri  = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
776✔
175

176
                        if ( ! is_string( $host ) ) {
776✔
177
                                return false;
×
178
                        }
179

180
                        if ( ! is_string( $uri ) ) {
776✔
181
                                return false;
×
182
                        }
183

184
                        $parsed_site_url    = wp_parse_url( site_url( self::$route ), PHP_URL_PATH );
776✔
185
                        $graphql_url        = ! empty( $parsed_site_url ) ? wp_unslash( $parsed_site_url ) : self::$route;
776✔
186
                        $parsed_request_url = wp_parse_url( $uri, PHP_URL_PATH );
776✔
187
                        $request_url        = ! empty( $parsed_request_url ) ? wp_unslash( $parsed_request_url ) : '';
776✔
188

189
                        // Determine if the route is indeed a graphql request
190
                        $is_graphql_http_request = str_replace( '/', '', $request_url ) === str_replace( '/', '', $graphql_url );
776✔
191
                }
192

193
                /**
194
                 * Filter whether the request is a GraphQL HTTP Request. Default is false, as the majority
195
                 * of WordPress requests are NOT GraphQL requests (at least today that's true 😆).
196
                 *
197
                 * The request has to "prove" that it is indeed an HTTP request via HTTP for
198
                 * this to be true.
199
                 *
200
                 * Different servers _might_ have different needs to determine whether a request
201
                 * is a GraphQL request.
202
                 *
203
                 * @param bool $is_graphql_http_request Whether the request is a GraphQL HTTP Request. Default false.
204
                 */
205
                return apply_filters( 'graphql_is_graphql_http_request', $is_graphql_http_request );
777✔
206
        }
207

208
        /**
209
         * This resolves the http request and ensures that WordPress can respond with the appropriate
210
         * JSON response instead of responding with a template from the standard WordPress Template
211
         * Loading process
212
         *
213
         * @return void
214
         * @throws \Exception Throws exception.
215
         * @throws \Throwable Throws exception.
216
         * @since  0.0.1
217
         */
218
        public static function resolve_http_request() {
85✔
219

220
                /**
221
                 * Access the $wp_query object
222
                 */
223
                global $wp_query;
85✔
224

225
                /**
226
                 * Ensure we're on the registered route for graphql route
227
                 */
228
                if ( ! self::is_graphql_http_request() || is_graphql_request() ) {
85✔
229
                        return;
85✔
230
                }
231

232
                /**
233
                 * Set is_home to false
234
                 */
235
                $wp_query->is_home = false;
×
236

237
                /**
238
                 * Whether it's a GraphQL HTTP Request
239
                 *
240
                 * @since 0.0.5
241
                 */
242
                if ( ! defined( 'GRAPHQL_HTTP_REQUEST' ) ) {
×
243
                        define( 'GRAPHQL_HTTP_REQUEST', true );
×
244
                }
245

246
                /**
247
                 * Process the GraphQL query Request
248
                 */
249
                self::process_http_request();
×
250
        }
251

252
        /**
253
         * Sends an HTTP header.
254
         *
255
         * @param string $key   Header key.
256
         * @param string $value Header value.
257
         *
258
         * @return void
259
         * @since  0.0.5
260
         */
261
        public static function send_header( $key, $value ) {
×
262

263
                /**
264
                 * Sanitize as per RFC2616 (Section 4.2):
265
                 *
266
                 * Any LWS that occurs between field-content MAY be replaced with a
267
                 * single SP before interpreting the field value or forwarding the
268
                 * message downstream.
269
                 */
270
                $value = preg_replace( '/\s+/', ' ', $value );
×
271
                header( apply_filters( 'graphql_send_header', sprintf( '%s: %s', $key, $value ), $key, $value ) );
×
272
        }
273

274
        /**
275
         * Sends an HTTP status code.
276
         *
277
         * @param int|null $status_code The status code to send.
278
         *
279
         * @return void
280
         */
281
        protected static function set_status( ?int $status_code = null ) {
×
282
                $status_code = null === $status_code ? self::$http_status_code : $status_code;
×
283

284
                // validate that the status code is a valid http status code
285
                if ( ! is_numeric( $status_code ) || $status_code < 100 || $status_code > 599 ) {
×
286
                        $status_code = 500;
×
287
                }
288

289
                status_header( $status_code );
×
290
        }
291

292
        /**
293
         * Returns an array of headers to send with the HTTP response
294
         *
295
         * @return array<string,string>
296
         */
297
        protected static function get_response_headers() {
×
298

299
                /**
300
                 * Filtered list of access control headers.
301
                 *
302
                 * @param string[] $access_control_headers Array of headers to allow.
303
                 */
304
                $access_control_allow_headers = apply_filters(
×
305
                        'graphql_access_control_allow_headers',
×
306
                        [
×
307
                                'Authorization',
×
308
                                'Content-Type',
×
309
                        ]
×
310
                );
×
311

312
                // For cache url header, use the domain without protocol. Path for when it's multisite.
313
                // Remove the starting http://, https://, :// from the full hostname/path.
314
                $host_and_path = preg_replace( '#^.*?://#', '', graphql_get_endpoint_url() );
×
315

316
                $headers = [
×
317
                        'Access-Control-Allow-Origin'  => '*',
×
318
                        'Access-Control-Allow-Headers' => implode( ', ', $access_control_allow_headers ),
×
319
                        'Access-Control-Max-Age'       => '600', // cache the result of preflight requests (600 is the upper limit for Chromium).
×
320
                        'Content-Type'                 => 'application/json ; charset=' . get_option( 'blog_charset' ),
×
321
                        'X-Robots-Tag'                 => 'noindex',
×
322
                        'X-Content-Type-Options'       => 'nosniff',
×
323
                        'X-GraphQL-URL'                => (string) $host_and_path,
×
324
                ];
×
325

326
                // If the Query Analyzer was instantiated
327
                // Get the headers determined from its Analysis
328
                if ( self::get_request() instanceof Request && self::get_request()->get_query_analyzer()->is_enabled_for_query() ) {
×
329
                        $headers = self::get_request()->get_query_analyzer()->get_headers( $headers );
×
330
                }
331

332
                if ( true === \WPGraphQL::debug() ) {
×
333
                        $headers['X-hacker'] = __( 'If you\'re reading this, you should visit github.com/wp-graphql/wp-graphql and contribute!', 'wp-graphql' );
×
334
                }
335

336
                /**
337
                 * Send nocache headers on authenticated requests.
338
                 *
339
                 * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
340
                 *
341
                 * @since 0.0.5
342
                 */
343
                $send_no_cache_headers = apply_filters( 'graphql_send_nocache_headers', is_user_logged_in() );
×
344
                if ( $send_no_cache_headers ) {
×
345
                        foreach ( wp_get_nocache_headers() as $no_cache_header_key => $no_cache_header_value ) {
×
346
                                $headers[ $no_cache_header_key ] = $no_cache_header_value;
×
347
                        }
348
                }
349

350
                /**
351
                 * Filter the $headers to send
352
                 *
353
                 * @param array<string,string> $headers The headers to send
354
                 */
355
                $headers = apply_filters( 'graphql_response_headers_to_send', $headers );
×
356

357
                return is_array( $headers ) ? $headers : [];
×
358
        }
359

360
        /**
361
         * Set the response headers
362
         *
363
         * @return void
364
         * @since  0.0.1
365
         */
366
        public static function set_headers() {
×
367
                if ( false === headers_sent() ) {
×
368

369
                        /**
370
                         * Set the HTTP response status
371
                         */
372
                        self::set_status( self::$http_status_code );
×
373

374
                        /**
375
                         * Get the response headers
376
                         */
377
                        $headers = self::get_response_headers();
×
378

379
                        /**
380
                         * If there are headers, set them for the response
381
                         */
382
                        if ( ! empty( $headers ) && is_array( $headers ) ) {
×
383
                                foreach ( $headers as $key => $value ) {
×
384
                                        self::send_header( $key, $value );
×
385
                                }
386
                        }
387

388
                        /**
389
                         * Fire an action when the headers are set
390
                         *
391
                         * @param array<string,string> $headers The headers sent in the response
392
                         */
393
                        do_action( 'graphql_response_set_headers', $headers );
×
394
                }
395
        }
396

397
        /**
398
         * Retrieves the raw request entity (body).
399
         *
400
         * @since  0.0.5
401
         *
402
         * @global string php://input Raw post data.
403
         *
404
         * @return string Raw request data.
405
         */
406
        public static function get_raw_data() {
2✔
407
                $input = file_get_contents( 'php://input' ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsRemoteFile
2✔
408

409
                return ! empty( $input ) ? $input : '';
2✔
410
        }
411

412
        /**
413
         * This processes the graphql requests that come into the /graphql endpoint via an HTTP request
414
         *
415
         * @return void
416
         * @throws \Throwable Throws Exception.
417
         * @global WP_User $current_user The currently authenticated user.
418
         * @since  0.0.1
419
         */
420
        public static function process_http_request() {
×
421
                global $current_user;
×
422

423
                if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
×
424
                        /*
425
                         * If there is no current user authenticated via other means, clear
426
                         * the cached lack of user, so that an authenticate check can set it
427
                         * properly.
428
                         *
429
                         * This is done because for authentications such as Application
430
                         * Passwords, we don't want it to be accepted unless the current HTTP
431
                         * request is a GraphQL API request, which can't always be identified early
432
                         * enough in evaluation.
433
                         *
434
                         * See serve_request in wp-includes/rest-api/class-wp-rest-server.php.
435
                         */
436
                        $current_user = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
×
437
                }
438

439
                /**
440
                 * This action can be hooked to to enable various debug tools,
441
                 * such as enableValidation from the GraphQL Config.
442
                 *
443
                 * @since 0.0.4
444
                 */
445
                do_action( 'graphql_process_http_request' );
×
446

447
                /**
448
                 * Respond to pre-flight requests.
449
                 *
450
                 * Bail before Request() execution begins.
451
                 *
452
                 * @see: https://apollographql.slack.com/archives/C10HTKHPC/p1507649812000123
453
                 * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
454
                 */
455
                if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) {
×
456
                        self::$http_status_code = 200;
×
457
                        self::set_headers();
×
458
                        exit;
×
459
                }
460

461
                $response       = [];
×
462
                $query          = '';
×
463
                $operation_name = '';
×
464
                $variables      = [];
×
465
                self::$request  = new Request();
×
466

467
                try {
468
                        // Start output buffering to prevent any unwanted output from breaking the JSON response
469
                        // This addresses issues like plugins calling wp_print_inline_script_tag() during wp_enqueue_scripts
NEW
470
                        ob_start();
×
471

UNCOV
472
                        $response = self::$request->execute_http();
×
473

474
                        // Discard any captured output that could break the JSON response
NEW
475
                        ob_end_clean();
×
476

477
                        // Get the operation params from the request.
478
                        $params         = self::$request->get_params();
×
479
                        $query          = isset( $params->query ) ? $params->query : '';
×
480
                        $operation_name = isset( $params->operation ) ? $params->operation : '';
×
481
                        $variables      = isset( $params->variables ) ? $params->variables : null;
×
482
                } catch ( \Throwable $error ) {
×
483
                        // Make sure to clean up the output buffer even if there's an exception
NEW
484
                        if ( ob_get_level() > 0 ) {
×
NEW
485
                                ob_end_clean();
×
486
                        }
487

488
                        /**
489
                         * If there are errors, set the status to 500
490
                         * and format the captured errors to be output properly
491
                         *
492
                         * @since 0.0.4
493
                         */
494
                        self::$http_status_code = 500;
×
495

496
                        /**
497
                         * Filter thrown GraphQL errors
498
                         *
499
                         * @var SerializableResult $response
500
                         *
501
                         * @param SerializableError[] $errors  The errors array to be sent in the response.
502
                         * @param \Throwable          $error   Thrown error object.
503
                         * @param \WPGraphQL\Request  $request WPGraphQL Request object.
504
                         */
505
                        $response['errors'] = apply_filters(
×
506
                                'graphql_http_request_response_errors',
×
507
                                [ FormattedError::createFromException( $error, self::$request->get_debug_flag() ) ],
×
508
                                $error,
×
509
                                self::$request
×
510
                        );
×
511
                }
512

513
                // Previously there was a small distinction between the response and the result, but
514
                // now that we are delegating to Request, just send the response for both.
515

516
                if ( false === headers_sent() ) {
×
517
                        self::prepare_headers( $response, $response, $query, $operation_name, $variables );
×
518
                }
519

520
                /**
521
                 * Run an action after the HTTP Response is ready to be sent back. This might be a good place for tools
522
                 * to hook in to track metrics, such as how long the process took from `graphql_process_http_request`
523
                 * to here, etc.
524
                 *
525
                 * @param WPGraphQLResult      $response       The GraphQL response
526
                 * @param WPGraphQLResult      $result         Deprecated. Same as $response.
527
                 * @param string               $operation_name The name of the operation
528
                 * @param string               $query          The request that GraphQL executed
529
                 * @param ?array<string,mixed> $variables      Variables to passed to your GraphQL query
530
                 * @param int|string           $status_code    The status code for the response
531
                 *
532
                 * @since 0.0.5
533
                 */
534
                do_action( 'graphql_process_http_request_response', $response, $response, $operation_name, $query, $variables, self::$http_status_code );
×
535

536
                /**
537
                 * Send the response
538
                 */
539
                wp_send_json( $response );
×
540
        }
541

542
        /**
543
         * Prepare headers for response
544
         *
545
         * @param mixed[]|\GraphQL\Executor\ExecutionResult $response       The response of the GraphQL Request.
546
         * @param mixed[]|\GraphQL\Executor\ExecutionResult $_deprecated    Deprecated.
547
         * @param string                                    $query          The GraphQL query.
548
         * @param string                                    $operation_name The operation name of the GraphQL Request.
549
         * @param ?array<string,mixed>                      $variables      The variables applied to the GraphQL Request.
550
         * @param ?\WP_User                                 $user           The current user object.
551
         *
552
         * @return void
553
         */
554
        protected static function prepare_headers( $response, $_deprecated, string $query, string $operation_name, $variables, $user = null ) {
×
555

556
                /**
557
                 * Filter the $status_code before setting the headers
558
                 *
559
                 * @param int                                       $status_code    The status code to apply to the headers
560
                 * @param mixed[]|\GraphQL\Executor\ExecutionResult $response       The response of the GraphQL Request
561
                 * @param mixed[]|\GraphQL\Executor\ExecutionResult $_deprecated    Use $response instead.
562
                 * @param string                                    $query          The GraphQL query
563
                 * @param string                                    $operation_name The operation name of the GraphQL Request
564
                 * @param ?array<string,mixed>                      $variables      The variables applied to the GraphQL Request
565
                 * @param ?\WP_User                                 $user           The current user object
566
                 */
567
                self::$http_status_code = apply_filters( 'graphql_response_status_code', self::$http_status_code, $_deprecated, $response, $query, $operation_name, $variables, $user );
×
568

569
                /**
570
                 * Set the response headers
571
                 */
572
                self::set_headers();
×
573
        }
574

575
        /**
576
         * @deprecated 0.4.1 Use Router::is_graphql_http_request instead. This now resolves to it
577
         * @todo remove in v3.0
578
         * @codeCoverageIgnore
579
         *
580
         * @return bool
581
         */
582
        public static function is_graphql_request() {
583
                _doing_it_wrong(
584
                        __METHOD__,
585
                        sprintf(
586
                                /* translators: %s is the class name */
587
                                esc_html__( 'This method is deprecated and will be removed in the next major version of WPGraphQL. Use %s instead.', 'wp-graphql' ),
588
                                esc_html( self::class . '::is_graphql_http_request()' )
589
                        ),
590
                        '0.4.1'
591
                );
592
                return self::is_graphql_http_request();
593
        }
594
}
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

© 2025 Coveralls, Inc