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

wp-graphql / wp-graphql / 15710056976

17 Jun 2025 02:27PM UTC coverage: 84.17% (-0.1%) from 84.287%
15710056976

push

github

actions-user
release: merge develop into master for v2.3.3

15925 of 18920 relevant lines covered (84.17%)

258.66 hits per line

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

20.61
/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() {
758✔
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 );
758✔
156

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

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

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

170
                        $is_graphql_http_request = true;
1✔
171
                } elseif ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
757✔
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
757✔
174
                        $uri  = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
757✔
175

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

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

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

189
                        // Determine if the route is indeed a graphql request
190
                        $is_graphql_http_request = str_replace( '/', '', $request_url ) === str_replace( '/', '', $graphql_url );
757✔
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 );
758✔
206
        }
207

208
        /**
209
         * DEPRECATED: Returns whether a request is a GraphQL Request. Deprecated
210
         * because it's name is a bit misleading. This will only return if the request
211
         * is a GraphQL request coming from the HTTP endpoint. Internal GraphQL requests
212
         * won't be able to use this to properly determine if the request is a GraphQL request
213
         * or not.
214
         *
215
         * @return bool
216
         * @deprecated 0.4.1 Use Router::is_graphql_http_request instead. This now resolves to it
217
         */
218
        public static function is_graphql_request() {
×
219
                _deprecated_function( __METHOD__, '0.4.1', self::class . 'is_graphql_http_request()' );
×
220
                return self::is_graphql_http_request();
×
221
        }
222

223
        /**
224
         * This resolves the http request and ensures that WordPress can respond with the appropriate
225
         * JSON response instead of responding with a template from the standard WordPress Template
226
         * Loading process
227
         *
228
         * @return void
229
         * @throws \Exception Throws exception.
230
         * @throws \Throwable Throws exception.
231
         * @since  0.0.1
232
         */
233
        public static function resolve_http_request() {
85✔
234

235
                /**
236
                 * Access the $wp_query object
237
                 */
238
                global $wp_query;
85✔
239

240
                /**
241
                 * Ensure we're on the registered route for graphql route
242
                 */
243
                if ( ! self::is_graphql_http_request() || is_graphql_request() ) {
85✔
244
                        return;
85✔
245
                }
246

247
                /**
248
                 * Set is_home to false
249
                 */
250
                $wp_query->is_home = false;
×
251

252
                /**
253
                 * Whether it's a GraphQL HTTP Request
254
                 *
255
                 * @since 0.0.5
256
                 */
257
                if ( ! defined( 'GRAPHQL_HTTP_REQUEST' ) ) {
×
258
                        define( 'GRAPHQL_HTTP_REQUEST', true );
×
259
                }
260

261
                /**
262
                 * Process the GraphQL query Request
263
                 */
264
                self::process_http_request();
×
265
        }
266

267
        /**
268
         * Sends an HTTP header.
269
         *
270
         * @param string $key   Header key.
271
         * @param string $value Header value.
272
         *
273
         * @return void
274
         * @since  0.0.5
275
         */
276
        public static function send_header( $key, $value ) {
×
277

278
                /**
279
                 * Sanitize as per RFC2616 (Section 4.2):
280
                 *
281
                 * Any LWS that occurs between field-content MAY be replaced with a
282
                 * single SP before interpreting the field value or forwarding the
283
                 * message downstream.
284
                 */
285
                $value = preg_replace( '/\s+/', ' ', $value );
×
286
                header( apply_filters( 'graphql_send_header', sprintf( '%s: %s', $key, $value ), $key, $value ) );
×
287
        }
288

289
        /**
290
         * Sends an HTTP status code.
291
         *
292
         * @param int|null $status_code The status code to send.
293
         *
294
         * @return void
295
         */
296
        protected static function set_status( ?int $status_code = null ) {
×
297
                $status_code = null === $status_code ? self::$http_status_code : $status_code;
×
298

299
                // validate that the status code is a valid http status code
300
                if ( ! is_numeric( $status_code ) || $status_code < 100 || $status_code > 599 ) {
×
301
                        $status_code = 500;
×
302
                }
303

304
                status_header( $status_code );
×
305
        }
306

307
        /**
308
         * Returns an array of headers to send with the HTTP response
309
         *
310
         * @return array<string,string>
311
         */
312
        protected static function get_response_headers() {
×
313

314
                /**
315
                 * Filtered list of access control headers.
316
                 *
317
                 * @param string[] $access_control_headers Array of headers to allow.
318
                 */
319
                $access_control_allow_headers = apply_filters(
×
320
                        'graphql_access_control_allow_headers',
×
321
                        [
×
322
                                'Authorization',
×
323
                                'Content-Type',
×
324
                        ]
×
325
                );
×
326

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

331
                $headers = [
×
332
                        'Access-Control-Allow-Origin'  => '*',
×
333
                        'Access-Control-Allow-Headers' => implode( ', ', $access_control_allow_headers ),
×
334
                        'Access-Control-Max-Age'       => '600', // cache the result of preflight requests (600 is the upper limit for Chromium).
×
335
                        'Content-Type'                 => 'application/json ; charset=' . get_option( 'blog_charset' ),
×
336
                        'X-Robots-Tag'                 => 'noindex',
×
337
                        'X-Content-Type-Options'       => 'nosniff',
×
338
                        'X-GraphQL-URL'                => (string) $host_and_path,
×
339
                ];
×
340

341
                // If the Query Analyzer was instantiated
342
                // Get the headers determined from its Analysis
343
                if ( self::get_request() instanceof Request && self::get_request()->get_query_analyzer()->is_enabled_for_query() ) {
×
344
                        $headers = self::get_request()->get_query_analyzer()->get_headers( $headers );
×
345
                }
346

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

351
                /**
352
                 * Send nocache headers on authenticated requests.
353
                 *
354
                 * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
355
                 *
356
                 * @since 0.0.5
357
                 */
358
                $send_no_cache_headers = apply_filters( 'graphql_send_nocache_headers', is_user_logged_in() );
×
359
                if ( $send_no_cache_headers ) {
×
360
                        foreach ( wp_get_nocache_headers() as $no_cache_header_key => $no_cache_header_value ) {
×
361
                                $headers[ $no_cache_header_key ] = $no_cache_header_value;
×
362
                        }
363
                }
364

365
                /**
366
                 * Filter the $headers to send
367
                 *
368
                 * @param array<string,string> $headers The headers to send
369
                 */
370
                $headers = apply_filters( 'graphql_response_headers_to_send', $headers );
×
371

372
                return is_array( $headers ) ? $headers : [];
×
373
        }
374

375
        /**
376
         * Set the response headers
377
         *
378
         * @return void
379
         * @since  0.0.1
380
         */
381
        public static function set_headers() {
×
382
                if ( false === headers_sent() ) {
×
383

384
                        /**
385
                         * Set the HTTP response status
386
                         */
387
                        self::set_status( self::$http_status_code );
×
388

389
                        /**
390
                         * Get the response headers
391
                         */
392
                        $headers = self::get_response_headers();
×
393

394
                        /**
395
                         * If there are headers, set them for the response
396
                         */
397
                        if ( ! empty( $headers ) && is_array( $headers ) ) {
×
398
                                foreach ( $headers as $key => $value ) {
×
399
                                        self::send_header( $key, $value );
×
400
                                }
401
                        }
402

403
                        /**
404
                         * Fire an action when the headers are set
405
                         *
406
                         * @param array<string,string> $headers The headers sent in the response
407
                         */
408
                        do_action( 'graphql_response_set_headers', $headers );
×
409
                }
410
        }
411

412
        /**
413
         * Retrieves the raw request entity (body).
414
         *
415
         * @since  0.0.5
416
         *
417
         * @global string php://input Raw post data.
418
         *
419
         * @return string Raw request data.
420
         */
421
        public static function get_raw_data() {
2✔
422
                $input = file_get_contents( 'php://input' ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsRemoteFile
2✔
423

424
                return ! empty( $input ) ? $input : '';
2✔
425
        }
426

427
        /**
428
         * This processes the graphql requests that come into the /graphql endpoint via an HTTP request
429
         *
430
         * @return void
431
         * @throws \Throwable Throws Exception.
432
         * @global WP_User $current_user The currently authenticated user.
433
         * @since  0.0.1
434
         */
435
        public static function process_http_request() {
×
436
                global $current_user;
×
437

438
                if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
×
439
                        /*
440
                         * If there is no current user authenticated via other means, clear
441
                         * the cached lack of user, so that an authenticate check can set it
442
                         * properly.
443
                         *
444
                         * This is done because for authentications such as Application
445
                         * Passwords, we don't want it to be accepted unless the current HTTP
446
                         * request is a GraphQL API request, which can't always be identified early
447
                         * enough in evaluation.
448
                         *
449
                         * See serve_request in wp-includes/rest-api/class-wp-rest-server.php.
450
                         */
451
                        $current_user = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
×
452
                }
453

454
                /**
455
                 * This action can be hooked to to enable various debug tools,
456
                 * such as enableValidation from the GraphQL Config.
457
                 *
458
                 * @since 0.0.4
459
                 */
460
                do_action( 'graphql_process_http_request' );
×
461

462
                /**
463
                 * Respond to pre-flight requests.
464
                 *
465
                 * Bail before Request() execution begins.
466
                 *
467
                 * @see: https://apollographql.slack.com/archives/C10HTKHPC/p1507649812000123
468
                 * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
469
                 */
470
                if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) {
×
471
                        self::$http_status_code = 200;
×
472
                        self::set_headers();
×
473
                        exit;
×
474
                }
475

476
                $response       = [];
×
477
                $query          = '';
×
478
                $operation_name = '';
×
479
                $variables      = [];
×
480
                self::$request  = new Request();
×
481

482
                try {
483
                        $response = self::$request->execute_http();
×
484

485
                        // Get the operation params from the request.
486
                        $params         = self::$request->get_params();
×
487
                        $query          = isset( $params->query ) ? $params->query : '';
×
488
                        $operation_name = isset( $params->operation ) ? $params->operation : '';
×
489
                        $variables      = isset( $params->variables ) ? $params->variables : null;
×
490
                } catch ( \Throwable $error ) {
×
491

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

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

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

520
                if ( false === headers_sent() ) {
×
521
                        self::prepare_headers( $response, $response, $query, $operation_name, $variables );
×
522
                }
523

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

540
                /**
541
                 * Send the response
542
                 */
543
                wp_send_json( $response );
×
544
        }
545

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

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

573
                /**
574
                 * Set the response headers
575
                 */
576
                self::set_headers();
×
577
        }
578
}
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