• 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

52.8
/src/Request.php
1
<?php
2

3
namespace WPGraphQL;
4

5
use Exception;
6
use GraphQL\Error\DebugFlag;
7
use GraphQL\Error\Error;
8
use GraphQL\GraphQL;
9
use GraphQL\Server\OperationParams;
10
use GraphQL\Server\ServerConfig;
11
use GraphQL\Server\StandardServer;
12
use WPGraphQL\Server\ValidationRules\DisableIntrospection;
13
use WPGraphQL\Server\ValidationRules\QueryDepth;
14
use WPGraphQL\Server\ValidationRules\RequireAuthentication;
15
use WPGraphQL\Server\WPHelper;
16
use WPGraphQL\Utils\DebugLog;
17
use WPGraphQL\Utils\QueryAnalyzer;
18

19
/**
20
 * Class Request
21
 *
22
 * Proxies a request to graphql-php, applying filters and transforming request
23
 * data as needed.
24
 *
25
 * @package WPGraphQL
26
 *
27
 * phpcs:disable -- PHPStan annotation.
28
 * @phpstan-import-type RootValueResolver from \GraphQL\Server\ServerConfig
29
 * @phpstan-import-type SerializableResult from \GraphQL\Executor\ExecutionResult
30
 * phpcs:enable
31
 */
32
class Request {
33

34
        /**
35
         * App context for this request.
36
         *
37
         * @var \WPGraphQL\AppContext
38
         */
39
        public $app_context;
40

41
        /**
42
         * Request data.
43
         *
44
         * @var array<string,mixed>|\GraphQL\Server\OperationParams
45
         */
46
        public $data;
47

48
        /**
49
         * Cached global post.
50
         *
51
         * @var ?\WP_Post
52
         */
53
        public $global_post;
54

55
        /**
56
         * Cached global wp_the_query.
57
         *
58
         * @var ?\WP_Query
59
         */
60
        private $global_wp_the_query;
61

62
        /**
63
         * GraphQL operation parameters for this request.
64
         * Will be an array of OperationParams if this is a batch request.
65
         *
66
         * @var \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[]
67
         */
68
        public $params;
69

70
        /**
71
         * Schema for this request.
72
         *
73
         * @var \WPGraphQL\WPSchema
74
         */
75
        public $schema;
76

77
        /**
78
         * Debug log for WPGraphQL Requests
79
         *
80
         * @var \WPGraphQL\Utils\DebugLog
81
         */
82
        public $debug_log;
83

84
        /**
85
         * The Type Registry the Schema is built with
86
         *
87
         * @var \WPGraphQL\Registry\TypeRegistry
88
         */
89
        public $type_registry;
90

91
        /**
92
         * Validation rules for execution.
93
         *
94
         * @var array<string,\GraphQL\Validator\Rules\ValidationRule>
95
         */
96
        protected $validation_rules;
97

98
        /**
99
         * The default field resolver function. Default null
100
         *
101
         * @var callable|null
102
         */
103
        protected $field_resolver;
104

105
        /**
106
         * The root value of the request. Default null;
107
         *
108
         * @var mixed|RootValueResolver
109
         */
110
        protected $root_value;
111

112
        /**
113
         * @var \WPGraphQL\Utils\QueryAnalyzer
114
         */
115
        protected $query_analyzer;
116

117
        /**
118
         * Constructor
119
         *
120
         * @param array<string,mixed> $data The request data (for Non-HTTP requests).
121
         *
122
         * @return void
123
         *
124
         * @throws \Exception
125
         */
126
        public function __construct( array $data = [] ) {
779✔
127

128
                /**
129
                 * Whether it's a GraphQL Request (http or internal)
130
                 *
131
                 * @since 0.0.5
132
                 */
133
                if ( ! defined( 'GRAPHQL_REQUEST' ) ) {
779✔
134
                        define( 'GRAPHQL_REQUEST', true );
×
135
                }
136

137
                /**
138
                 * Filter "is_graphql_request" to return true
139
                 */
140
                \WPGraphQL::set_is_graphql_request( true );
779✔
141

142
                /**
143
                 * Action – intentionally with no context – to indicate a GraphQL Request has started.
144
                 * This is a great place for plugins to hook in and modify things that should only
145
                 * occur in the context of a GraphQL Request. The base class hooks into this action to
146
                 * kick off the schema creation, so types are not set up until this action has run!
147
                 */
148
                do_action( 'init_graphql_request' );
779✔
149

150
                // Start tracking debug log messages
151
                $this->debug_log = new DebugLog();
779✔
152

153
                // Set request data for passed-in (non-HTTP) requests.
154
                $this->data = $data;
779✔
155

156
                // Get the Type Registry
157
                $this->type_registry = \WPGraphQL::get_type_registry();
779✔
158

159
                // Get the App Context
160
                $this->app_context = \WPGraphQL::get_app_context();
779✔
161

162
                $this->validation_rules = $this->get_validation_rules();
779✔
163
                $this->field_resolver   = $this->get_field_resolver();
779✔
164

165
                // Inject the type registry into the app context.
166
                $this->app_context->type_registry = $this->type_registry;
779✔
167

168
                // The query analyzer tracks nodes, models, list types and more
169
                // to return in headers and debug messages to help developers understand
170
                // what was resolved, how to cache it, etc.
171
                $this->query_analyzer = new QueryAnalyzer( $this );
779✔
172
                $this->query_analyzer->init();
779✔
173
        }
174

175
        /**
176
         * Get the instance of the Query Analyzer
177
         */
178
        public function get_query_analyzer(): QueryAnalyzer {
11✔
179
                return $this->query_analyzer;
11✔
180
        }
181

182
        /**
183
         * @return callable|null
184
         */
185
        protected function get_field_resolver() {
779✔
186
                return $this->field_resolver;
779✔
187
        }
188

189
        /**
190
         * Return the validation rules to use in the request
191
         *
192
         * @return array<string,\GraphQL\Validator\Rules\ValidationRule>
193
         */
194
        protected function get_validation_rules(): array {
779✔
195
                $validation_rules = GraphQL::getStandardValidationRules();
779✔
196

197
                $validation_rules['require_authentication'] = new RequireAuthentication();
779✔
198
                $validation_rules['disable_introspection']  = new DisableIntrospection();
779✔
199
                $validation_rules['query_depth']            = new QueryDepth();
779✔
200

201
                /**
202
                 * Return the validation rules to use in the request
203
                 *
204
                 * @param array<string,\GraphQL\Validator\Rules\ValidationRule> $validation_rules The validation rules to use in the request
205
                 * @param \WPGraphQL\Request                                        $request          The Request instance
206
                 */
207
                return apply_filters( 'graphql_validation_rules', $validation_rules, $this );
779✔
208
        }
209

210
        /**
211
         * Returns the root value to use in the request.
212
         *
213
         * @return mixed|RootValueResolver|null
214
         */
215
        protected function get_root_value() {
772✔
216
                /**
217
                 * Set the root value based on what was passed to the request
218
                 */
219
                $root_value = is_array( $this->data ) && ! empty( $this->data['root_value'] ) ? $this->data['root_value'] : null;
772✔
220

221
                /**
222
                 * Return the filtered root value
223
                 *
224
                 * @param mixed|RootValueResolver $root_value The root value the Schema should use to resolve with. Default null.
225
                 * @param \WPGraphQL\Request      $request    The Request instance
226
                 */
227
                return apply_filters( 'graphql_root_value', $root_value, $this );
772✔
228
        }
229

230
        /**
231
         * Apply filters and do actions before GraphQL execution
232
         *
233
         * @throws \GraphQL\Error\Error
234
         */
235
        private function before_execute(): void {
773✔
236

237
                /**
238
                 * Store the global post so that it can be reset after GraphQL execution
239
                 *
240
                 * This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode
241
                 * without disrupting the flow of the post as the global POST before and after GraphQL execution will be
242
                 * the same.
243
                 */
244
                if ( ! empty( $GLOBALS['post'] ) ) {
773✔
245
                        $this->global_post = $GLOBALS['post'];
237✔
246
                }
247

248
                if ( ! empty( $GLOBALS['wp_query'] ) ) {
773✔
249
                        $this->global_wp_the_query = clone $GLOBALS['wp_the_query'];
773✔
250
                }
251

252
                /**
253
                 * If the request is a batch request it will come back as an array
254
                 */
255
                if ( is_array( $this->params ) ) {
773✔
256

257
                        // If the request is a batch request, but batch requests are disabled,
258
                        // bail early
259
                        if ( ! $this->is_batch_queries_enabled() ) {
×
260
                                throw new Error( esc_html__( 'Batch Queries are not supported', 'wp-graphql' ) );
×
261
                        }
262

263
                        $batch_limit = get_graphql_setting( 'batch_limit', 10 );
×
264
                        $batch_limit = absint( $batch_limit ) ? absint( $batch_limit ) : 10;
×
265

266
                        // If batch requests are enabled, but a limit is set and the request exceeds the limit
267
                        // fail now
268
                        if ( $batch_limit < count( $this->params ) ) {
×
269
                                // translators: First placeholder is the max number of batch operations allowed in a GraphQL request. The 2nd placeholder is the number of operations requested in the current request.
270
                                throw new Error( sprintf( esc_html__( 'Batch requests are limited to %1$d operations. This request contained %2$d', 'wp-graphql' ), absint( $batch_limit ), count( $this->params ) ) );
×
271
                        }
272

273
                        /**
274
                         * Execute batch queries
275
                         *
276
                         * @param \GraphQL\Server\OperationParams[] $params The operation params of the batch request
277
                         */
278
                        do_action( 'graphql_execute_batch_queries', $this->params );
×
279

280
                        // Process the batched requests
281
                        array_walk( $this->params, [ $this, 'do_action' ] );
×
282
                } else {
283
                        $this->do_action( $this->params );
773✔
284
                }
285

286
                // Get the Schema
287
                $this->schema = \WPGraphQL::get_schema();
772✔
288

289
                /**
290
                 * This action runs before execution of a GraphQL request (regardless if it's a single or batch request)
291
                 *
292
                 * @param \WPGraphQL\Request $request The instance of the Request being executed
293
                 */
294
                do_action( 'graphql_before_execute', $this );
772✔
295
        }
296

297
        /**
298
         * Checks authentication errors.
299
         *
300
         * False will mean there are no detected errors and
301
         * execution will continue.
302
         *
303
         * Anything else (true, WP_Error, thrown exception, etc) will prevent execution of the GraphQL
304
         * request.
305
         *
306
         * @return bool
307
         * @throws \Exception
308
         */
309
        protected function has_authentication_errors() {
772✔
310
                /**
311
                 * Bail if this is not an HTTP request.
312
                 *
313
                 * Auth for internal requests will happen
314
                 * via WordPress internals.
315
                 */
316
                if ( ! is_graphql_http_request() ) {
772✔
317
                        return false;
772✔
318
                }
319

320
                /**
321
                 * Access the global $wp_rest_auth_cookie
322
                 */
323
                global $wp_rest_auth_cookie;
×
324

325
                /**
326
                 * Default state of the authentication errors
327
                 */
328
                $authentication_errors = false;
×
329

330
                /**
331
                 * Is cookie authentication NOT being used?
332
                 *
333
                 * If we get an auth error, but the user is still logged in, another auth mechanism
334
                 * (JWT, oAuth, etc) must have been used.
335
                 */
336
                if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
×
337

338
                        /**
339
                         * Return filtered authentication errors
340
                         */
341
                        return $this->filtered_authentication_errors( $authentication_errors );
×
342
                }
343

344
                /**
345
                 * If the user is not logged in, determine if there's a nonce
346
                 */
347
                $nonce = null;
×
348

349
                if ( isset( $_REQUEST['_wpnonce'] ) ) {
×
350
                        $nonce = $_REQUEST['_wpnonce']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
×
351
                } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
×
352
                        $nonce = $_SERVER['HTTP_X_WP_NONCE']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
×
353
                }
354

355
                if ( null === $nonce ) {
×
356
                        // No nonce at all, so act as if it's an unauthenticated request.
357
                        wp_set_current_user( 0 );
×
358

359
                        return $this->filtered_authentication_errors( $authentication_errors );
×
360
                }
361

362
                // Check the nonce.
363
                $result = wp_verify_nonce( $nonce, 'wp_rest' );
×
364

365
                if ( ! $result ) {
×
366
                        throw new Exception( esc_html__( 'Cookie nonce is invalid', 'wp-graphql' ) );
×
367
                }
368

369
                /**
370
                 * Return the filtered authentication errors
371
                 */
372
                return $this->filtered_authentication_errors( $authentication_errors );
×
373
        }
374

375
        /**
376
         * Filter Authentication errors. Allows plugins that authenticate to hook in and prevent
377
         * execution if Authentication errors exist.
378
         *
379
         * @param bool $authentication_errors Whether there are authentication errors with the request.
380
         *
381
         * @return bool
382
         */
383
        protected function filtered_authentication_errors( $authentication_errors = false ) {
×
384

385
                /**
386
                 * If false, there are no authentication errors. If true, execution of the
387
                 * GraphQL request will be prevented and an error will be thrown.
388
                 *
389
                 * @param bool $authentication_errors Whether there are authentication errors with the request
390
                 * @param \WPGraphQL\Request $request Instance of the Request
391
                 */
392
                return apply_filters( 'graphql_authentication_errors', $authentication_errors, $this );
×
393
        }
394

395
        /**
396
         * Performs actions and runs filters after execution completes
397
         *
398
         * @template T from (SerializableResult|SerializableResult[])|(\GraphQL\Executor\ExecutionResult|array<int,\GraphQL\Executor\ExecutionResult>)
399
         *
400
         * @param T $response The response from execution.  Array for batch requests, single object for individual requests.
401
         *
402
         * @return T
403
         *
404
         * @throws \Exception
405
         */
406
        private function after_execute( $response ) {
772✔
407

408
                /**
409
                 * If there are authentication errors, prevent execution and throw an exception.
410
                 */
411
                if ( false !== $this->has_authentication_errors() ) {
772✔
412
                        throw new Exception( esc_html__( 'Authentication Error', 'wp-graphql' ) );
×
413
                }
414

415
                /**
416
                 * If the params and the $response are both arrays
417
                 * treat this as a batch request and map over the array to apply the
418
                 * after_execute_actions, otherwise apply them to the current response
419
                 */
420
                if ( is_array( $this->params ) && is_array( $response ) ) {
772✔
421
                        $filtered_response = [];
×
422
                        foreach ( $response as $key => $resp ) {
×
423
                                $filtered_response[] = $this->after_execute_actions( $resp, (int) $key );
×
424
                        }
425
                } else {
426
                        $filtered_response = $this->after_execute_actions( $response, null );
772✔
427
                }
428

429
                /**
430
                 * Reset the global post after execution
431
                 *
432
                 * This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode
433
                 * without disrupting the flow of the post as the global POST before and after GraphQL execution will be
434
                 * the same.
435
                 *
436
                 * We cannot use wp_reset_postdata here because it just resets the post from the global query which can
437
                 * be anything the because the resolvers themself can set it to whatever. So we just manually reset the
438
                 * post with setup_postdata we cached before this request.
439
                 */
440

441
                if ( ! empty( $this->global_wp_the_query ) ) {
772✔
442
                        $GLOBALS['wp_the_query'] = $this->global_wp_the_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
772✔
443
                        wp_reset_query(); // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query
772✔
444
                }
445

446
                if ( ! empty( $this->global_post ) ) {
772✔
447
                        $GLOBALS['post'] = $this->global_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
237✔
448
                        setup_postdata( $this->global_post );
237✔
449
                }
450

451
                /**
452
                 * Run an action after GraphQL Execution
453
                 *
454
                 * @param mixed[]            $filtered_response The response of the entire operation. Could be a single operation or a batch operation
455
                 * @param \WPGraphQL\Request $request           Instance of the Request being executed
456
                 */
457
                do_action( 'graphql_after_execute', $filtered_response, $this );
772✔
458

459
                /**
460
                 * Return the filtered response
461
                 */
462
                return $filtered_response;
772✔
463
        }
464

465
        /**
466
         * Apply filters and do actions after GraphQL execution
467
         *
468
         * @param mixed|array<string,mixed>|object $response The response for your GraphQL request
469
         * @param int|null                         $key      The array key of the params for batch requests
470
         *
471
         * @return mixed|array<string,mixed>|object
472
         */
473
        private function after_execute_actions( $response, $key = null ) {
772✔
474

475
                /**
476
                 * Determine which params (batch or single request) to use when passing through to the actions
477
                 */
478
                $query     = null;
772✔
479
                $operation = null;
772✔
480
                $variables = null;
772✔
481
                $query_id  = null;
772✔
482

483
                if ( $this->params instanceof OperationParams ) {
772✔
484
                        $operation = $this->params->operation;
772✔
485
                        $query     = $this->params->query;
772✔
486
                        $query_id  = $this->params->queryId;
772✔
487
                        $variables = $this->params->variables;
772✔
488
                } elseif ( is_array( $this->params ) ) {
×
489
                        $operation = $this->params[ $key ]->operation ?? '';
×
490
                        $query     = $this->params[ $key ]->query ?? '';
×
491
                        $query_id  = $this->params[ $key ]->queryId ?? null;
×
492
                        $variables = $this->params[ $key ]->variables ?? null;
×
493
                }
494

495
                /**
496
                 * Run an action. This is a good place for debug tools to hook in to log things, etc.
497
                 *
498
                 * @param mixed|array<string,mixed>|object $response  The response your GraphQL request
499
                 * @param \WPGraphQL\WPSchema              $schema    The schema object for the root request
500
                 * @param ?string                          $operation The name of the operation
501
                 * @param ?string                          $query     The query that GraphQL executed
502
                 * @param ?array<string,mixed>             $variables Variables to passed to your GraphQL query
503
                 * @param \WPGraphQL\Request               $request   Instance of the Request
504
                 *
505
                 * @since 0.0.4
506
                 */
507
                do_action( 'graphql_execute', $response, $this->schema, $operation, $query, $variables, $this );
772✔
508

509
                /**
510
                 * Add the debug log to the request
511
                 */
512
                if ( ! empty( $response ) ) {
772✔
513
                        if ( is_array( $response ) ) {
772✔
514
                                $response['extensions']['debug'] = $this->debug_log->get_logs();
772✔
515
                        } else {
516
                                $response->extensions['debug'] = $this->debug_log->get_logs();
×
517
                        }
518
                }
519

520
                /**
521
                 * Filter the $response of the GraphQL execution. This allows for the response to be filtered
522
                 * before it's returned, allowing granular control over the response at the latest point.
523
                 *
524
                 * POSSIBLE USAGE EXAMPLES:
525
                 * This could be used to ensure that certain fields never make it to the response if they match
526
                 * certain criteria, etc. For example, this filter could be used to check if a current user is
527
                 * allowed to see certain things, and if they are not, the $response could be filtered to remove
528
                 * the data they should not be allowed to see.
529
                 *
530
                 * Or, perhaps some systems want the response to always include some additional piece of data in
531
                 * every response, regardless of the request that was sent to it, this could allow for that
532
                 * to be hooked in and included in the $response.
533
                 *
534
                 * @param mixed|array<string,mixed>|object $response  The response for your GraphQL query
535
                 * @param \WPGraphQL\WPSchema              $schema    The schema object for the root request
536
                 * @param ?string                          $operation The name of the operation
537
                 * @param ?string                          $query     The query that GraphQL executed
538
                 * @param ?array<string,mixed>             $variables Variables to passed to your GraphQL query
539
                 * @param \WPGraphQL\Request               $request   Instance of the Request
540
                 * @param ?string                          $query_id  The query id that GraphQL executed
541
                 *
542
                 * @since 0.0.5
543
                 */
544
                $filtered_response = apply_filters( 'graphql_request_results', $response, $this->schema, $operation, $query, $variables, $this, $query_id );
772✔
545

546
                /**
547
                 * Run an action after the response has been filtered, as the response is being returned.
548
                 * This is a good place for debug tools to hook in to log things, etc.
549
                 *
550
                 * @param mixed|array<string,mixed>|object $filtered_response The filtered response for the GraphQL request
551
                 * @param mixed|array<string,mixed>|object $response          The response for your GraphQL request
552
                 * @param \WPGraphQL\WPSchema              $schema            The schema object for the root request
553
                 * @param ?string                          $operation         The name of the operation
554
                 * @param ?string                          $query             The query that GraphQL executed
555
                 * @param ?array<string,mixed>             $variables         Variables to passed to your GraphQL query
556
                 * @param \WPGraphQL\Request               $request           Instance of the Request
557
                 * @param ?string                          $query_id          The query id that GraphQL executed
558
                 */
559
                do_action( 'graphql_return_response', $filtered_response, $response, $this->schema, $operation, $query, $variables, $this, $query_id );
772✔
560

561
                /**
562
                 * Filter "is_graphql_request" back to false.
563
                 */
564
                \WPGraphQL::set_is_graphql_request( false );
772✔
565

566
                return $filtered_response;
772✔
567
        }
568

569
        /**
570
         * Run action for a request.
571
         *
572
         * @param \GraphQL\Server\OperationParams $params OperationParams for the request.
573
         */
574
        private function do_action( OperationParams $params ): void {
773✔
575

576
                /**
577
                 * Run an action for each request.
578
                 *
579
                 * @param ?string                         $query     The GraphQL query
580
                 * @param ?string                         $operation The name of the operation
581
                 * @param ?array<string,mixed>            $variables Variables to be passed to your GraphQL request
582
                 * @param \GraphQL\Server\OperationParams $params    The Operation Params. This includes any extra params,
583
                 *                                                   such as extensions or any other modifications to the request body
584
                 */
585
                do_action( 'do_graphql_request', $params->query, $params->operation, $params->variables, $params );
773✔
586
        }
587

588
        /**
589
         * Execute an internal request (graphql() function call).
590
         *
591
         * @return mixed[]
592
         * @phpstan-return SerializableResult|SerializableResult[]|mixed[]
593
         * @throws \Exception
594
         */
595
        public function execute() {
773✔
596
                $helper = new WPHelper();
773✔
597

598
                if ( ! $this->data instanceof OperationParams ) {
773✔
599
                        $this->params = $helper->parseRequestParams( 'POST', $this->data, [] );
773✔
600
                } else {
601
                        $this->params = $this->data;
3✔
602
                }
603

604
                if ( is_array( $this->params ) ) {
773✔
605
                        return array_map(
3✔
606
                                function ( $data ) {
3✔
607
                                        $this->data = $data;
3✔
608
                                        return $this->execute();
3✔
609
                                },
3✔
610
                                $this->params
3✔
611
                        );
3✔
612
                }
613

614
                // If $this->params isn't an array or an OperationParams instance, then something probably went wrong.
615
                if ( ! $this->params instanceof OperationParams ) {
773✔
616
                        throw new \Exception( 'Invalid request params.' );
×
617
                }
618

619
                /**
620
                 * Initialize the GraphQL Request
621
                 */
622
                $this->before_execute();
773✔
623

624
                /**
625
                 * Filter this to be anything other than null to short-circuit the request.
626
                 *
627
                 * @param ?SerializableResult $response
628
                 * @param self               $request
629
                 */
630
                $response = apply_filters( 'pre_graphql_execute_request', null, $this );
772✔
631

632
                if ( null === $response ) {
772✔
633
                        /**
634
                         * @var \GraphQL\Server\OperationParams $params
635
                         */
636
                        $params = $this->params;
772✔
637

638
                        /**
639
                         * Allow the query string to be determined by a filter. Ex, when params->queryId is present, query can be retrieved.
640
                         *
641
                         * @param string                          $query
642
                         * @param \GraphQL\Server\OperationParams $params
643
                         */
644
                        $query = apply_filters(
772✔
645
                                'graphql_execute_query_params',
772✔
646
                                $params->query ?? '',
772✔
647
                                $params
772✔
648
                        );
772✔
649

650
                        $result = GraphQL::executeQuery(
772✔
651
                                $this->schema,
772✔
652
                                $query,
772✔
653
                                $this->get_root_value(),
772✔
654
                                $this->app_context,
772✔
655
                                $params->variables ?? null,
772✔
656
                                $params->operation ?? null,
772✔
657
                                $this->field_resolver,
772✔
658
                                $this->validation_rules
772✔
659
                        );
772✔
660

661
                        /**
662
                         * Return the result of the request
663
                         */
664
                        $response = $result->toArray( $this->get_debug_flag() );
772✔
665
                }
666

667
                /**
668
                 * Ensure the response is returned as a proper, populated array. Otherwise add an error.
669
                 */
670
                if ( empty( $response ) || ! is_array( $response ) ) {
772✔
671
                        $response = [
×
672
                                'errors' => __( 'The GraphQL request returned an invalid response', 'wp-graphql' ),
×
673
                        ];
×
674
                }
675

676
                /**
677
                 * If the request is a batch request it will come back as an array
678
                 */
679
                return $this->after_execute( $response );
772✔
680
        }
681

682
        /**
683
         * Execute an HTTP request.
684
         *
685
         * @return SerializableResult|(\GraphQL\Executor\ExecutionResult|array<int,\GraphQL\Executor\ExecutionResult>)
686
         * @throws \Exception
687
         */
688
        public function execute_http() {
×
689
                if ( ! $this->is_valid_http_content_type() ) {
×
690
                        return $this->get_invalid_content_type_response();
×
691
                }
692

693
                /**
694
                 * Parse HTTP request.
695
                 */
696
                $helper       = new WPHelper();
×
697
                $this->params = $helper->parseHttpRequest();
×
698

699
                /**
700
                 * Initialize the GraphQL Request
701
                 */
702
                $this->before_execute();
×
703

704
                /**
705
                 * Get the response.
706
                 */
707
                $response = apply_filters( 'pre_graphql_execute_request', null, $this );
×
708

709
                /**
710
                 * If no cached response, execute the query
711
                 */
712
                if ( null === $response ) {
×
713
                        $server   = $this->get_server();
×
714
                        $response = $server->executeRequest( $this->params );
×
715
                }
716

717
                return $this->after_execute( $response );
×
718
        }
719

720
        /**
721
         * Validates the content type for HTTP POST requests
722
         */
723
        private function is_valid_http_content_type(): bool {
×
724
                if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
×
725
                        return true;
×
726
                }
727

728
                $content_type = $this->get_content_type();
×
729
                if ( empty( $content_type ) ) {
×
730
                        return false;
×
731
                }
732

733
                $is_valid = 0 === stripos( $content_type, 'application/json' );
×
734

735
                /**
736
                 * Allow graphql to validate custom content types for HTTP POST requests
737
                 *
738
                 * @param bool $is_valid Whether the content type is valid
739
                 * @param string $content_type The content type header value that was received
740
                 *
741
                 * @since 2.1.0
742
                 */
743
                return (bool) apply_filters( 'graphql_is_valid_http_content_type', $is_valid, $content_type );
×
744
        }
745

746
        /**
747
         * Gets the content type from the request headers
748
         */
749
        private function get_content_type(): string {
×
750
                if ( isset( $_SERVER['CONTENT_TYPE'] ) ) {
×
751
                        return sanitize_text_field( $_SERVER['CONTENT_TYPE'] );
×
752
                }
753

754
                if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
×
755
                        return sanitize_text_field( $_SERVER['HTTP_CONTENT_TYPE'] );
×
756
                }
757

758
                return '';
×
759
        }
760

761
        /**
762
         * Returns the error response for invalid content type
763
         *
764
         * @return array{errors: array{array{message: string}}}
765
         */
766
        private function get_invalid_content_type_response(): array {
×
767
                $content_type = $this->get_content_type();
×
768

769
                /**
770
                 * Filter the status code to return when the content type is invalid
771
                 *
772
                 * @param int    $status_code The status code to return. Default 415.
773
                 * @param string $content_type The content type header value that was received.
774
                 */
775
                $filtered_status_code = apply_filters( 'graphql_invalid_content_type_status_code', 415, $content_type );
×
776

777
                // Set the status code to the filtered value if it's a valid status code.
778
                if ( is_numeric( $filtered_status_code ) ) {
×
779
                        $filtered_status_code = (int) $filtered_status_code;
×
780

781
                        if ( $filtered_status_code > 100 && $filtered_status_code < 599 ) {
×
782
                                Router::$http_status_code = $filtered_status_code;
×
783
                        }
784
                }
785

786
                return [
×
787
                        'errors' => [
×
788
                                [
×
789
                                        // translators: %s is the content type header value that was received
790
                                        'message' => sprintf( esc_html__( 'HTTP POST requests must have Content-Type: application/json header. Received: %s', 'wp-graphql' ), $content_type ),
×
791
                                ],
×
792
                        ],
×
793
                ];
×
794
        }
795

796
        /**
797
         * Get the operation params for the request.
798
         *
799
         * @return \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[]
800
         */
801
        public function get_params() {
3✔
802
                return $this->params;
3✔
803
        }
804

805
        /**
806
         * Returns the debug flag value
807
         *
808
         * @return int
809
         */
810
        public function get_debug_flag() {
772✔
811
                $flag = DebugFlag::INCLUDE_DEBUG_MESSAGE;
772✔
812
                if ( 0 !== get_current_user_id() ) {
772✔
813
                        // Flag 2 shows the trace data, which should require user to be logged in to see by default
814
                        $flag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
278✔
815
                }
816

817
                return true === \WPGraphQL::debug() ? $flag : DebugFlag::NONE;
772✔
818
        }
819

820
        /**
821
         * Determines if batch queries are enabled for the server.
822
         *
823
         * Default is to have batch queries enabled.
824
         */
825
        private function is_batch_queries_enabled(): bool {
×
826
                $batch_queries_enabled = true;
×
827

828
                $batch_queries_setting = get_graphql_setting( 'batch_queries_enabled', 'on' );
×
829
                if ( 'off' === $batch_queries_setting ) {
×
830
                        $batch_queries_enabled = false;
×
831
                }
832

833
                /**
834
                 * Filter whether batch queries are supported or not
835
                 *
836
                 * @param bool                                                              $batch_queries_enabled Whether Batch Queries should be enabled
837
                 * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params Request operation params
838
                 */
839
                return (bool) apply_filters( 'graphql_is_batch_queries_enabled', $batch_queries_enabled, $this->params );
×
840
        }
841

842
        /**
843
         * Create the GraphQL server that will process the request.
844
         */
845
        private function get_server(): StandardServer {
×
846
                $debug_flag = $this->get_debug_flag();
×
847

848
                $config = new ServerConfig();
×
849
                $config
×
850
                        ->setDebugFlag( $debug_flag )
×
851
                        ->setSchema( $this->schema )
×
852
                        ->setContext( $this->app_context )
×
853
                        ->setValidationRules( $this->validation_rules )
×
854
                        ->setQueryBatching( $this->is_batch_queries_enabled() );
×
855

NEW
856
                if ( ! empty( $this->get_root_value() ) ) {
×
NEW
857
                        $config->setRootValue( $this->get_root_value() );
×
858
                }
859

860
                if ( ! empty( $this->field_resolver ) ) {
×
861
                        $config->setFieldResolver( $this->field_resolver );
×
862
                }
863

864
                /**
865
                 * Run an action when the server config is created. The config can be acted
866
                 * upon directly to override default values or implement new features, e.g.,
867
                 * $config->setValidationRules().
868
                 *
869
                 * @param \GraphQL\Server\ServerConfig                                      $config Server config
870
                 * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params Request operation params
871
                 *
872
                 * @since 0.2.0
873
                 */
874
                do_action( 'graphql_server_config', $config, $this->params );
×
875

876
                return new StandardServer( $config );
×
877
        }
878
}
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