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

wp-graphql / wp-graphql / 13316763745

13 Feb 2025 08:45PM UTC coverage: 82.712% (-0.3%) from 83.023%
13316763745

push

github

web-flow
Merge pull request #3307 from wp-graphql/release/v2.0.0

release: v2.0.0

195 of 270 new or added lines in 20 files covered. (72.22%)

180 existing lines in 42 files now uncovered.

13836 of 16728 relevant lines covered (82.71%)

299.8 hits per line

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

20.77
/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
class Router {
16

17
        /**
18
         * Sets the route to use as the endpoint
19
         *
20
         * @var string $route
21
         */
22
        public static $route = 'graphql';
23

24
        /**
25
         * Holds the Global Post for later resetting
26
         *
27
         * @var string
28
         */
29
        protected static $global_post = '';
30

31
        /**
32
         * Set the default status code to 200.
33
         *
34
         * @var int
35
         */
36
        public static $http_status_code = 200;
37

38
        /**
39
         * @var \WPGraphQL\Request | null
40
         */
41
        protected static $request;
42

43
        /**
44
         * Initialize the WPGraphQL Router
45
         *
46
         * @return void
47
         * @throws \Exception
48
         */
UNCOV
49
        public function init() {
×
50
                self::$route = graphql_get_endpoint();
×
51

52
                /**
53
                 * Create the rewrite rule for the route
54
                 *
55
                 * @since 0.0.1
56
                 */
57
                add_action( 'init', [ $this, 'add_rewrite_rule' ], 10 );
×
58

59
                /**
60
                 * Add the query var for the route
61
                 *
62
                 * @since 0.0.1
63
                 */
64
                add_filter( 'query_vars', [ $this, 'add_query_var' ], 1, 1 );
×
65

66
                /**
67
                 * Redirects the route to the graphql processor
68
                 *
69
                 * @since 0.0.1
70
                 */
71
                add_action( 'parse_request', [ $this, 'resolve_http_request' ], 10 );
×
72

73
                /**
74
                 * Adds support for application passwords
75
                 */
76
                add_filter( 'application_password_is_api_request', [ $this, 'is_api_request' ] );
×
77
        }
78

79
        /**
80
         * Returns the GraphQL Request being executed
81
         */
UNCOV
82
        public static function get_request(): ?Request {
×
83
                return self::$request;
×
84
        }
85

86
        /**
87
         * Adds rewrite rule for the route endpoint
88
         *
89
         * @return void
90
         * @since  0.0.1
91
         * @uses   add_rewrite_rule()
92
         */
UNCOV
93
        public static function add_rewrite_rule() {
×
94
                add_rewrite_rule(
×
95
                        self::$route . '/?$',
×
96
                        'index.php?' . self::$route . '=true',
×
97
                        'top'
×
98
                );
×
99
        }
100

101
        /**
102
         * Determines whether the request is an API request to play nice with
103
         * application passwords and potential other WordPress core functionality
104
         * for APIs
105
         *
106
         * @param bool $is_api_request Whether the request is an API request
107
         *
108
         * @return bool
109
         */
UNCOV
110
        public function is_api_request( $is_api_request ) {
×
111
                return true === is_graphql_http_request() ? true : $is_api_request;
×
112
        }
113

114
        /**
115
         * Adds the query_var for the route
116
         *
117
         * @param string[] $query_vars The array of whitelisted query variables.
118
         *
119
         * @return string[]
120
         * @since  0.0.1
121
         */
122
        public static function add_query_var( $query_vars ) {
86✔
123
                $query_vars[] = self::$route;
86✔
124

125
                return $query_vars;
86✔
126
        }
127

128
        /**
129
         * Returns true when the current request is a GraphQL request coming from the HTTP
130
         *
131
         * NOTE: This will only indicate whether the GraphQL Request is an HTTP request. Many features
132
         * need to affect _all_ GraphQL requests, including internal requests using the `graphql()`
133
         * function, so be careful how you use this to check your conditions.
134
         *
135
         * @return bool
136
         */
137
        public static function is_graphql_http_request() {
752✔
138

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

150
                /**
151
                 * If the filter has been applied, return now before executing default checks
152
                 */
153
                if ( null !== $pre_is_graphql_http_request ) {
752✔
154
                        return (bool) $pre_is_graphql_http_request;
×
155
                }
156

157
                // Default is false
158
                $is_graphql_http_request = false;
752✔
159

160
                // Support wp-graphiql style request to /index.php?graphql.
161
                if ( isset( $_GET[ self::$route ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
752✔
162

163
                        $is_graphql_http_request = true;
1✔
164
                } elseif ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
751✔
165
                        // Check the server to determine if the GraphQL endpoint is being requested
166
                        $host = wp_unslash( $_SERVER['HTTP_HOST'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
751✔
167
                        $uri  = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
751✔
168

169
                        if ( ! is_string( $host ) ) {
751✔
170
                                return false;
×
171
                        }
172

173
                        if ( ! is_string( $uri ) ) {
751✔
174
                                return false;
×
175
                        }
176

177
                        $parsed_site_url    = wp_parse_url( site_url( self::$route ), PHP_URL_PATH );
751✔
178
                        $graphql_url        = ! empty( $parsed_site_url ) ? wp_unslash( $parsed_site_url ) : self::$route;
751✔
179
                        $parsed_request_url = wp_parse_url( $uri, PHP_URL_PATH );
751✔
180
                        $request_url        = ! empty( $parsed_request_url ) ? wp_unslash( $parsed_request_url ) : '';
751✔
181

182
                        // Determine if the route is indeed a graphql request
183
                        $is_graphql_http_request = str_replace( '/', '', $request_url ) === str_replace( '/', '', $graphql_url );
751✔
184
                }
185

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

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

216
        /**
217
         * This resolves the http request and ensures that WordPress can respond with the appropriate
218
         * JSON response instead of responding with a template from the standard WordPress Template
219
         * Loading process
220
         *
221
         * @return void
222
         * @throws \Exception Throws exception.
223
         * @throws \Throwable Throws exception.
224
         * @since  0.0.1
225
         */
226
        public static function resolve_http_request() {
85✔
227

228
                /**
229
                 * Access the $wp_query object
230
                 */
231
                global $wp_query;
85✔
232

233
                /**
234
                 * Ensure we're on the registered route for graphql route
235
                 */
236
                if ( ! self::is_graphql_http_request() || is_graphql_request() ) {
85✔
237
                        return;
85✔
238
                }
239

240
                /**
241
                 * Set is_home to false
242
                 */
243
                $wp_query->is_home = false;
×
244

245
                /**
246
                 * Whether it's a GraphQL HTTP Request
247
                 *
248
                 * @since 0.0.5
249
                 */
250
                if ( ! defined( 'GRAPHQL_HTTP_REQUEST' ) ) {
×
251
                        define( 'GRAPHQL_HTTP_REQUEST', true );
×
252
                }
253

254
                /**
255
                 * Process the GraphQL query Request
256
                 */
257
                self::process_http_request();
×
258
        }
259

260
        /**
261
         * Sends an HTTP header.
262
         *
263
         * @param string $key   Header key.
264
         * @param string $value Header value.
265
         *
266
         * @return void
267
         * @since  0.0.5
268
         */
UNCOV
269
        public static function send_header( $key, $value ) {
×
270

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

282
        /**
283
         * Sends an HTTP status code.
284
         *
285
         * @param int|null $status_code The status code to send.
286
         *
287
         * @return void
288
         */
NEW
289
        protected static function set_status( ?int $status_code = null ) {
×
NEW
290
                $status_code = null === $status_code ? self::$http_status_code : $status_code;
×
291

292
                // validate that the status code is a valid http status code
NEW
293
                if ( ! is_numeric( $status_code ) || $status_code < 100 || $status_code > 599 ) {
×
NEW
294
                        $status_code = 500;
×
295
                }
296

NEW
297
                status_header( $status_code );
×
298
        }
299

300
        /**
301
         * Returns an array of headers to send with the HTTP response
302
         *
303
         * @return array<string,mixed>
304
         */
UNCOV
305
        protected static function get_response_headers() {
×
306

307
                /**
308
                 * Filtered list of access control headers.
309
                 *
310
                 * @param string[] $access_control_headers Array of headers to allow.
311
                 */
312
                $access_control_allow_headers = apply_filters(
×
313
                        'graphql_access_control_allow_headers',
×
314
                        [
×
315
                                'Authorization',
×
316
                                'Content-Type',
×
317
                        ]
×
318
                );
×
319

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

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

335
                // If the Query Analyzer was instantiated
336
                // Get the headers determined from its Analysis
337
                if ( self::get_request() instanceof Request && self::get_request()->get_query_analyzer()->is_enabled_for_query() ) {
×
338
                        $headers = self::get_request()->get_query_analyzer()->get_headers( $headers );
×
339
                }
340

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

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

359
                /**
360
                 * Filter the $headers to send
361
                 */
362
                return apply_filters( 'graphql_response_headers_to_send', $headers );
×
363
        }
364

365
        /**
366
         * Set the response headers
367
         *
368
         * @return void
369
         * @since  0.0.1
370
         */
UNCOV
371
        public static function set_headers() {
×
372
                if ( false === headers_sent() ) {
×
373

374
                        /**
375
                         * Set the HTTP response status
376
                         */
NEW
377
                        self::set_status( self::$http_status_code );
×
378

379
                        /**
380
                         * Get the response headers
381
                         */
382
                        $headers = self::get_response_headers();
×
383

384
                        /**
385
                         * If there are headers, set them for the response
386
                         */
387
                        if ( ! empty( $headers ) && is_array( $headers ) ) {
×
388
                                foreach ( $headers as $key => $value ) {
×
389
                                        self::send_header( $key, $value );
×
390
                                }
391
                        }
392

393
                        /**
394
                         * Fire an action when the headers are set
395
                         *
396
                         * @param array<string,mixed> $headers The headers sent in the response
397
                         */
398
                        do_action( 'graphql_response_set_headers', $headers );
×
399
                }
400
        }
401

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

414
                return ! empty( $input ) ? $input : '';
2✔
415
        }
416

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

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

445
                /**
446
                 * This action can be hooked to to enable various debug tools,
447
                 * such as enableValidation from the GraphQL Config.
448
                 *
449
                 * @since 0.0.4
450
                 */
451
                do_action( 'graphql_process_http_request' );
×
452

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

467
                $query          = '';
×
468
                $operation_name = '';
×
469
                $variables      = [];
×
470
                self::$request  = new Request();
×
471

472
                try {
473
                        $response = self::$request->execute_http();
×
474

475
                        // Get the operation params from the request.
476
                        $params         = self::$request->get_params();
×
477
                        $query          = isset( $params->query ) ? $params->query : '';
×
478
                        $operation_name = isset( $params->operation ) ? $params->operation : '';
×
479
                        $variables      = isset( $params->variables ) ? $params->variables : null;
×
480
                } catch ( \Throwable $error ) {
×
481

482
                        /**
483
                         * If there are errors, set the status to 500
484
                         * and format the captured errors to be output properly
485
                         *
486
                         * @since 0.0.4
487
                         */
488
                        self::$http_status_code = 500;
×
489

490
                        /**
491
                         * Filter thrown GraphQL errors
492
                         *
493
                         * @param mixed[]             $errors  Formatted errors object.
494
                         * @param \Throwable          $error   Thrown error.
495
                         * @param \WPGraphQL\Request  $request WPGraphQL Request object.
496
                         */
497
                        $response['errors'] = apply_filters(
×
498
                                'graphql_http_request_response_errors',
×
499
                                [ FormattedError::createFromException( $error, self::$request->get_debug_flag() ) ],
×
500
                                $error,
×
501
                                self::$request
×
502
                        );
×
503
                }
504

505
                // Previously there was a small distinction between the response and the result, but
506
                // now that we are delegating to Request, just send the response for both.
507
                $result = $response;
×
508

509
                if ( false === headers_sent() ) {
×
510
                        self::prepare_headers( $response, $result, $query, $operation_name, $variables );
×
511
                }
512

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

529
                /**
530
                 * Send the response
531
                 */
532
                wp_send_json( $response );
×
533
        }
534

535
        /**
536
         * Prepare headers for response
537
         *
538
         * @param mixed|array<string,mixed>|\GraphQL\Executor\ExecutionResult $response        The response of the GraphQL Request.
539
         * @param mixed|array<string,mixed>|\GraphQL\Executor\ExecutionResult $graphql_results The results of the GraphQL execution.
540
         * @param string                                                      $query           The GraphQL query.
541
         * @param string                                                      $operation_name  The operation name of the GraphQL Request.
542
         * @param mixed|array<string,mixed>|null                              $variables       The variables applied to the GraphQL Request.
543
         * @param mixed|\WP_User|null                                         $user            The current user object.
544
         *
545
         * @return void
546
         */
UNCOV
547
        protected static function prepare_headers( $response, $graphql_results, string $query, string $operation_name, $variables, $user = null ) {
×
548

549
                /**
550
                 * Filter the $status_code before setting the headers
551
                 *
552
                 * @param int      $status_code     The status code to apply to the headers
553
                 * @param array    $response        The response of the GraphQL Request
554
                 * @param array    $graphql_results The results of the GraphQL execution
555
                 * @param string   $query           The GraphQL query
556
                 * @param string   $operation_name  The operation name of the GraphQL Request
557
                 * @param mixed[]  $variables       The variables applied to the GraphQL Request
558
                 * @param \WP_User $user The current user object
559
                 */
560
                self::$http_status_code = apply_filters( 'graphql_response_status_code', self::$http_status_code, $response, $graphql_results, $query, $operation_name, $variables, $user );
×
561

562
                /**
563
                 * Set the response headers
564
                 */
565
                self::set_headers();
×
566
        }
567
}
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