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

wp-graphql / wp-graphql / 20311247040

17 Dec 2025 05:15PM UTC coverage: 83.642% (+0.02%) from 83.619%
20311247040

push

github

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

16296 of 19483 relevant lines covered (83.64%)

264.62 hits per line

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

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

3
namespace WPGraphQL;
4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

116
        /**
117
         * Authentication error stored during before_execute().
118
         * If set, the request should return this error instead of executing the query.
119
         *
120
         * @var \WP_Error|bool|null
121
         */
122
        protected $authentication_error = null;
123

124
        /**
125
         * Constructor
126
         *
127
         * @param array<string,mixed> $data The request data (for Non-HTTP requests).
128
         *
129
         * @return void
130
         *
131
         * @throws \Exception
132
         */
133
        public function __construct( array $data = [] ) {
800✔
134

135
                /**
136
                 * Whether it's a GraphQL Request (http or internal)
137
                 *
138
                 * @since 0.0.5
139
                 */
140
                if ( ! defined( 'GRAPHQL_REQUEST' ) ) {
800✔
141
                        define( 'GRAPHQL_REQUEST', true );
×
142
                }
143

144
                /**
145
                 * Filter "is_graphql_request" to return true
146
                 */
147
                \WPGraphQL::set_is_graphql_request( true );
800✔
148

149
                /**
150
                 * Action – intentionally with no context – to indicate a GraphQL Request has started.
151
                 * This is a great place for plugins to hook in and modify things that should only
152
                 * occur in the context of a GraphQL Request. The base class hooks into this action to
153
                 * kick off the schema creation, so types are not set up until this action has run!
154
                 */
155
                do_action( 'init_graphql_request' );
800✔
156

157
                // Start tracking debug log messages
158
                $this->debug_log = new DebugLog();
800✔
159

160
                // Set request data for passed-in (non-HTTP) requests.
161
                $this->data = $data;
800✔
162

163
                // Get the Type Registry
164
                $this->type_registry = \WPGraphQL::get_type_registry();
800✔
165

166
                // Get the App Context
167
                $this->app_context = \WPGraphQL::get_app_context();
800✔
168

169
                $this->validation_rules = $this->get_validation_rules();
800✔
170
                $this->field_resolver   = $this->get_field_resolver();
800✔
171

172
                // Inject the type registry into the app context.
173
                $this->app_context->type_registry = $this->type_registry;
800✔
174

175
                // The query analyzer tracks nodes, models, list types and more
176
                // to return in headers and debug messages to help developers understand
177
                // what was resolved, how to cache it, etc.
178
                $this->query_analyzer = new QueryAnalyzer( $this );
800✔
179
                $this->query_analyzer->init();
800✔
180
        }
181

182
        /**
183
         * Get the instance of the Query Analyzer
184
         */
185
        public function get_query_analyzer(): QueryAnalyzer {
11✔
186
                return $this->query_analyzer;
11✔
187
        }
188

189
        /**
190
         * @return callable|null
191
         */
192
        protected function get_field_resolver() {
800✔
193
                return $this->field_resolver;
800✔
194
        }
195

196
        /**
197
         * Return the validation rules to use in the request
198
         *
199
         * @return array<string,\GraphQL\Validator\Rules\ValidationRule>
200
         */
201
        protected function get_validation_rules(): array {
800✔
202
                $validation_rules = GraphQL::getStandardValidationRules();
800✔
203

204
                $validation_rules['require_authentication'] = new RequireAuthentication();
800✔
205
                $validation_rules['disable_introspection']  = new DisableIntrospection();
800✔
206
                $validation_rules['query_depth']            = new QueryDepth();
800✔
207

208
                /**
209
                 * Return the validation rules to use in the request
210
                 *
211
                 * @param array<string,\GraphQL\Validator\Rules\ValidationRule> $validation_rules The validation rules to use in the request
212
                 * @param \WPGraphQL\Request                                        $request          The Request instance
213
                 */
214
                return apply_filters( 'graphql_validation_rules', $validation_rules, $this );
800✔
215
        }
216

217
        /**
218
         * Returns the root value to use in the request.
219
         *
220
         * @return mixed|RootValueResolver|null
221
         */
222
        protected function get_root_value() {
793✔
223
                /**
224
                 * Set the root value based on what was passed to the request
225
                 */
226
                $root_value = is_array( $this->data ) && ! empty( $this->data['root_value'] ) ? $this->data['root_value'] : null;
793✔
227

228
                /**
229
                 * Return the filtered root value
230
                 *
231
                 * @param mixed|RootValueResolver $root_value The root value the Schema should use to resolve with. Default null.
232
                 * @param \WPGraphQL\Request      $request    The Request instance
233
                 */
234
                return apply_filters( 'graphql_root_value', $root_value, $this );
793✔
235
        }
236

237
        /**
238
         * Apply filters and do actions before GraphQL execution
239
         *
240
         * @throws \GraphQL\Error\Error
241
         */
242
        private function before_execute(): void {
794✔
243

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

255
                if ( ! empty( $GLOBALS['wp_query'] ) && $GLOBALS['wp_the_query'] instanceof \WP_Query ) {
794✔
256
                        $this->global_wp_the_query = clone $GLOBALS['wp_the_query'];
794✔
257
                }
258

259
                /**
260
                 * Reset authentication error state for this execution.
261
                 *
262
                 * This ensures each batch item starts with clean auth state, preventing
263
                 * errors from one batch item incorrectly persisting to subsequent items.
264
                 *
265
                 * @since next-version
266
                 */
267
                $this->authentication_error = null;
794✔
268

269
                /**
270
                 * Check for authentication errors via the graphql_authentication_errors filter.
271
                 *
272
                 * Note: For HTTP requests, all CSRF protection and nonce validation is
273
                 * handled by Router::validate_http_request_authentication() before this
274
                 * code runs. This call allows plugins to hook in and indicate auth errors.
275
                 *
276
                 * @since next-version CSRF protection and nonce validation moved to Router.
277
                 */
278
                $auth_error = $this->has_authentication_errors();
794✔
279

280
                if ( false !== $auth_error ) {
794✔
281
                        // Store the authentication error for later use in execute methods
282
                        $this->authentication_error = $auth_error;
1✔
283
                }
284

285
                /**
286
                 * Update AppContext->viewer to reflect the current user after auth check.
287
                 *
288
                 * If the user was downgraded due to missing nonce (CSRF protection),
289
                 * the viewer should reflect the guest user, not the originally authenticated user.
290
                 *
291
                 * @since 2.6.0
292
                 */
293
                $this->app_context->viewer = wp_get_current_user();
794✔
294

295
                /**
296
                 * If the request is a batch request it will come back as an array
297
                 */
298
                if ( is_array( $this->params ) ) {
794✔
299

300
                        // If the request is a batch request, but batch requests are disabled,
301
                        // bail early
302
                        if ( ! $this->is_batch_queries_enabled() ) {
×
303
                                throw new Error( esc_html__( 'Batch Queries are not supported', 'wp-graphql' ) );
×
304
                        }
305

306
                        $batch_limit = get_graphql_setting( 'batch_limit', 10 );
×
307
                        $batch_limit = absint( $batch_limit ) ? absint( $batch_limit ) : 10;
×
308

309
                        // If batch requests are enabled, but a limit is set and the request exceeds the limit
310
                        // fail now
311
                        if ( $batch_limit < count( $this->params ) ) {
×
312
                                // 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.
313
                                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 ) ) );
×
314
                        }
315

316
                        /**
317
                         * Execute batch queries
318
                         *
319
                         * @param \GraphQL\Server\OperationParams[] $params The operation params of the batch request
320
                         */
321
                        do_action( 'graphql_execute_batch_queries', $this->params );
×
322

323
                        // Process the batched requests
324
                        array_walk( $this->params, [ $this, 'do_action' ] );
×
325
                } else {
326
                        $this->do_action( $this->params );
794✔
327
                }
328

329
                // Get the Schema
330
                $this->schema = \WPGraphQL::get_schema();
793✔
331

332
                /**
333
                 * This action runs before execution of a GraphQL request (regardless if it's a single or batch request)
334
                 *
335
                 * @param \WPGraphQL\Request $request The instance of the Request being executed
336
                 */
337
                do_action( 'graphql_before_execute', $this );
793✔
338
        }
339

340
        /**
341
         * Checks authentication errors via the graphql_authentication_errors filter.
342
         *
343
         * As of 2.6.0, all CSRF protection and nonce validation for HTTP requests is
344
         * handled by Router::validate_http_request_authentication() BEFORE any GraphQL
345
         * hooks fire. This method now only provides:
346
         * - Plugin integration via the graphql_authentication_errors filter
347
         *
348
         * False means no errors and execution continues.
349
         * True or WP_Error prevents execution of the GraphQL request.
350
         *
351
         * @since 0.0.5
352
         * @since 2.6.0 CSRF protection and nonce validation moved to Router.
353
         *
354
         * @return bool|\WP_Error False if no errors, true or WP_Error if there are errors.
355
         *
356
         * @see Router::validate_http_request_authentication()
357
         */
358
        protected function has_authentication_errors() {
794✔
359
                return $this->filtered_authentication_errors( false );
794✔
360
        }
361

362
        /**
363
         * Filter Authentication errors. Allows plugins that authenticate to hook in and prevent
364
         * execution if Authentication errors exist.
365
         *
366
         * @param bool $authentication_errors Whether there are authentication errors with the request.
367
         *
368
         * @return bool
369
         */
370
        protected function filtered_authentication_errors( $authentication_errors = false ) {
794✔
371

372
                /**
373
                 * If false, there are no authentication errors. If true, execution of the
374
                 * GraphQL request will be prevented and an error will be thrown.
375
                 *
376
                 * @param bool $authentication_errors Whether there are authentication errors with the request
377
                 * @param \WPGraphQL\Request $request Instance of the Request
378
                 */
379
                return apply_filters( 'graphql_authentication_errors', $authentication_errors, $this );
794✔
380
        }
381

382
        /**
383
         * Performs actions and runs filters after execution completes
384
         *
385
         * @template T from (SerializableResult|SerializableResult[])|(\GraphQL\Executor\ExecutionResult|array<int,\GraphQL\Executor\ExecutionResult>)
386
         *
387
         * @param T $response The response from execution.  Array for batch requests, single object for individual requests.
388
         *
389
         * @return T
390
         */
391
        private function after_execute( $response ) {
793✔
392

393
                /**
394
                 * Authentication check has been moved to before_execute() as of 2.6.0.
395
                 * This ensures auth is validated BEFORE query execution, not after.
396
                 *
397
                 * @since 2.6.0 Auth check moved to before_execute()
398
                 * @see https://github.com/wp-graphql/wp-graphql/issues/3447
399
                 */
400

401
                /**
402
                 * If the params and the $response are both arrays
403
                 * treat this as a batch request and map over the array to apply the
404
                 * after_execute_actions, otherwise apply them to the current response
405
                 */
406
                if ( is_array( $this->params ) && is_array( $response ) ) {
793✔
407
                        $filtered_response = [];
×
408
                        foreach ( $response as $key => $resp ) {
×
409
                                $filtered_response[] = $this->after_execute_actions( $resp, (int) $key );
×
410
                        }
411
                } else {
412
                        $filtered_response = $this->after_execute_actions( $response, null );
793✔
413
                }
414

415
                /**
416
                 * Reset the global post after execution
417
                 *
418
                 * This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode
419
                 * without disrupting the flow of the post as the global POST before and after GraphQL execution will be
420
                 * the same.
421
                 *
422
                 * We cannot use wp_reset_postdata here because it just resets the post from the global query which can
423
                 * be anything the because the resolvers themself can set it to whatever. So we just manually reset the
424
                 * post with setup_postdata we cached before this request.
425
                 */
426

427
                if ( ! empty( $this->global_wp_the_query ) ) {
793✔
428
                        $GLOBALS['wp_the_query'] = $this->global_wp_the_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
793✔
429
                        wp_reset_query(); // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query
793✔
430
                }
431

432
                if ( ! empty( $this->global_post ) ) {
793✔
433
                        $GLOBALS['post'] = $this->global_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
242✔
434
                        setup_postdata( $this->global_post );
242✔
435
                }
436

437
                /**
438
                 * Run an action after GraphQL Execution
439
                 *
440
                 * @param mixed[]            $filtered_response The response of the entire operation. Could be a single operation or a batch operation
441
                 * @param \WPGraphQL\Request $request           Instance of the Request being executed
442
                 */
443
                do_action( 'graphql_after_execute', $filtered_response, $this );
793✔
444

445
                /**
446
                 * Return the filtered response
447
                 */
448
                return $filtered_response;
793✔
449
        }
450

451
        /**
452
         * Apply filters and do actions after GraphQL execution
453
         *
454
         * @param mixed|array<string,mixed>|object $response The response for your GraphQL request
455
         * @param int|null                         $key      The array key of the params for batch requests
456
         *
457
         * @return mixed|array<string,mixed>|object
458
         */
459
        private function after_execute_actions( $response, $key = null ) {
793✔
460

461
                /**
462
                 * Determine which params (batch or single request) to use when passing through to the actions
463
                 */
464
                $query     = null;
793✔
465
                $operation = null;
793✔
466
                $variables = null;
793✔
467
                $query_id  = null;
793✔
468

469
                if ( $this->params instanceof OperationParams ) {
793✔
470
                        $operation = $this->params->operation;
793✔
471
                        $query     = $this->params->query;
793✔
472
                        $query_id  = $this->params->queryId;
793✔
473
                        $variables = $this->params->variables;
793✔
474
                } elseif ( is_array( $this->params ) ) {
×
475
                        $operation = $this->params[ $key ]->operation ?? '';
×
476
                        $query     = $this->params[ $key ]->query ?? '';
×
477
                        $query_id  = $this->params[ $key ]->queryId ?? null;
×
478
                        $variables = $this->params[ $key ]->variables ?? null;
×
479
                }
480

481
                /**
482
                 * Run an action. This is a good place for debug tools to hook in to log things, etc.
483
                 *
484
                 * @param mixed|array<string,mixed>|object $response  The response your GraphQL request
485
                 * @param \WPGraphQL\WPSchema              $schema    The schema object for the root request
486
                 * @param ?string                          $operation The name of the operation
487
                 * @param ?string                          $query     The query that GraphQL executed
488
                 * @param ?array<string,mixed>             $variables Variables to passed to your GraphQL query
489
                 * @param \WPGraphQL\Request               $request   Instance of the Request
490
                 *
491
                 * @since 0.0.4
492
                 */
493
                do_action( 'graphql_execute', $response, $this->schema, $operation, $query, $variables, $this );
793✔
494

495
                /**
496
                 * Add the debug log to the request
497
                 */
498
                if ( ! empty( $response ) ) {
793✔
499
                        if ( is_array( $response ) ) {
793✔
500
                                $response['extensions']['debug'] = $this->debug_log->get_logs();
793✔
501
                        } else {
502
                                $response->extensions['debug'] = $this->debug_log->get_logs();
×
503
                        }
504
                }
505

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

532
                /**
533
                 * Run an action after the response has been filtered, as the response is being returned.
534
                 * This is a good place for debug tools to hook in to log things, etc.
535
                 *
536
                 * @param mixed|array<string,mixed>|object $filtered_response The filtered response for the GraphQL request
537
                 * @param mixed|array<string,mixed>|object $response          The response for your GraphQL request
538
                 * @param \WPGraphQL\WPSchema              $schema            The schema object for the root request
539
                 * @param ?string                          $operation         The name of the operation
540
                 * @param ?string                          $query             The query that GraphQL executed
541
                 * @param ?array<string,mixed>             $variables         Variables to passed to your GraphQL query
542
                 * @param \WPGraphQL\Request               $request           Instance of the Request
543
                 * @param ?string                          $query_id          The query id that GraphQL executed
544
                 */
545
                do_action( 'graphql_return_response', $filtered_response, $response, $this->schema, $operation, $query, $variables, $this, $query_id );
793✔
546

547
                /**
548
                 * Filter "is_graphql_request" back to false.
549
                 */
550
                \WPGraphQL::set_is_graphql_request( false );
793✔
551

552
                return $filtered_response;
793✔
553
        }
554

555
        /**
556
         * Run action for a request.
557
         *
558
         * @param \GraphQL\Server\OperationParams $params OperationParams for the request.
559
         */
560
        private function do_action( OperationParams $params ): void {
794✔
561

562
                /**
563
                 * Run an action for each request.
564
                 *
565
                 * @param ?string                         $query     The GraphQL query
566
                 * @param ?string                         $operation The name of the operation
567
                 * @param ?array<string,mixed>            $variables Variables to be passed to your GraphQL request
568
                 * @param \GraphQL\Server\OperationParams $params    The Operation Params. This includes any extra params,
569
                 *                                                   such as extensions or any other modifications to the request body
570
                 */
571
                do_action( 'do_graphql_request', $params->query, $params->operation, $params->variables, $params );
794✔
572
        }
573

574
        /**
575
         * Execute an internal request (graphql() function call).
576
         *
577
         * @return mixed[]
578
         * @phpstan-return SerializableResult|SerializableResult[]|mixed[]
579
         * @throws \Exception
580
         */
581
        public function execute() {
794✔
582
                $helper = new WPHelper();
794✔
583

584
                if ( ! $this->data instanceof OperationParams ) {
794✔
585
                        $this->params = $helper->parseRequestParams( 'POST', $this->data, [] );
794✔
586
                } else {
587
                        $this->params = $this->data;
4✔
588
                }
589

590
                if ( is_array( $this->params ) ) {
794✔
591
                        return array_map(
4✔
592
                                function ( $data ) {
4✔
593
                                        $this->data = $data;
4✔
594
                                        return $this->execute();
4✔
595
                                },
4✔
596
                                $this->params
4✔
597
                        );
4✔
598
                }
599

600
                // If $this->params isn't an array or an OperationParams instance, then something probably went wrong.
601
                if ( ! $this->params instanceof OperationParams ) {
794✔
602
                        throw new \Exception( 'Invalid request params.' );
×
603
                }
604

605
                /**
606
                 * Initialize the GraphQL Request
607
                 */
608
                $this->before_execute();
794✔
609

610
                /**
611
                 * If there was an authentication error, return it as a GraphQL error response
612
                 * instead of executing the query.
613
                 *
614
                 * IMPORTANT: This intentionally happens BEFORE the `pre_graphql_execute_request` filter.
615
                 * Authentication failures should fail fast for security reasons:
616
                 * - Don't give plugins a chance to interfere with or "undo" auth failures
617
                 * - Avoid unnecessary filter processing for failed requests
618
                 * - Ensure consistent, predictable auth error handling
619
                 *
620
                 * Plugins that need to observe ALL requests (including auth failures) should use
621
                 * earlier hooks like `graphql_before_execute` or `do_graphql_request`.
622
                 */
623
                if ( null !== $this->authentication_error ) {
793✔
624
                        $error_message = is_wp_error( $this->authentication_error )
1✔
625
                                ? $this->authentication_error->get_error_message()
1✔
626
                                : __( 'Authentication Error', 'wp-graphql' );
×
627

628
                        return $this->after_execute(
1✔
629
                                [
1✔
630
                                        'errors' => [
1✔
631
                                                [
1✔
632
                                                        'message' => esc_html( $error_message ),
1✔
633
                                                ],
1✔
634
                                        ],
1✔
635
                                ]
1✔
636
                        );
1✔
637
                }
638

639
                /**
640
                 * Filter this to be anything other than null to short-circuit the request.
641
                 *
642
                 * @param ?SerializableResult $response
643
                 * @param self               $request
644
                 */
645
                $response = apply_filters( 'pre_graphql_execute_request', null, $this );
793✔
646

647
                if ( null === $response ) {
793✔
648
                        /**
649
                         * @var \GraphQL\Server\OperationParams $params
650
                         */
651
                        $params = $this->params;
793✔
652

653
                        /**
654
                         * Allow the query string to be determined by a filter. Ex, when params->queryId is present, query can be retrieved.
655
                         *
656
                         * @param string                          $query
657
                         * @param \GraphQL\Server\OperationParams $params
658
                         */
659
                        $query = apply_filters(
793✔
660
                                'graphql_execute_query_params',
793✔
661
                                $params->query ?? '',
793✔
662
                                $params
793✔
663
                        );
793✔
664

665
                        $result = GraphQL::executeQuery(
793✔
666
                                $this->schema,
793✔
667
                                $query,
793✔
668
                                $this->get_root_value(),
793✔
669
                                $this->app_context,
793✔
670
                                $params->variables ?? null,
793✔
671
                                $params->operation ?? null,
793✔
672
                                $this->field_resolver,
793✔
673
                                $this->validation_rules
793✔
674
                        );
793✔
675

676
                        /**
677
                         * Return the result of the request
678
                         */
679
                        $response = $result->toArray( $this->get_debug_flag() );
793✔
680
                }
681

682
                /**
683
                 * Ensure the response is returned as a proper, populated array. Otherwise add an error.
684
                 */
685
                if ( empty( $response ) || ! is_array( $response ) ) {
793✔
686
                        $response = [
×
687
                                'errors' => __( 'The GraphQL request returned an invalid response', 'wp-graphql' ),
×
688
                        ];
×
689
                }
690

691
                /**
692
                 * If the request is a batch request it will come back as an array
693
                 */
694
                return $this->after_execute( $response );
793✔
695
        }
696

697
        /**
698
         * Execute an HTTP request.
699
         *
700
         * @return SerializableResult|(\GraphQL\Executor\ExecutionResult|array<int,\GraphQL\Executor\ExecutionResult>)
701
         * @throws \Exception
702
         */
703
        public function execute_http() {
×
704
                if ( ! $this->is_valid_http_content_type() ) {
×
705
                        return $this->get_invalid_content_type_response();
×
706
                }
707

708
                /**
709
                 * Parse HTTP request.
710
                 */
711
                $helper       = new WPHelper();
×
712
                $this->params = $helper->parseHttpRequest();
×
713

714
                /**
715
                 * Initialize the GraphQL Request
716
                 */
717
                $this->before_execute();
×
718

719
                /**
720
                 * If there was an authentication error, return it as a GraphQL error response
721
                 * instead of executing the query. This ensures consistent error handling.
722
                 */
723
                if ( null !== $this->authentication_error ) {
×
724
                        $error_message = is_wp_error( $this->authentication_error )
×
725
                                ? $this->authentication_error->get_error_message()
×
726
                                : __( 'Authentication Error', 'wp-graphql' );
×
727

728
                        return $this->after_execute(
×
729
                                [
×
730
                                        'errors' => [
×
731
                                                [
×
732
                                                        'message' => esc_html( $error_message ),
×
733
                                                ],
×
734
                                        ],
×
735
                                ]
×
736
                        );
×
737
                }
738

739
                /**
740
                 * Get the response.
741
                 */
742
                $response = apply_filters( 'pre_graphql_execute_request', null, $this );
×
743

744
                /**
745
                 * If no cached response, execute the query
746
                 */
747
                if ( null === $response ) {
×
748
                        $server   = $this->get_server();
×
749
                        $response = $server->executeRequest( $this->params );
×
750
                }
751

752
                return $this->after_execute( $response );
×
753
        }
754

755
        /**
756
         * Validates the content type for HTTP POST requests
757
         */
758
        private function is_valid_http_content_type(): bool {
×
759
                if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
×
760
                        return true;
×
761
                }
762

763
                $content_type = $this->get_content_type();
×
764
                if ( empty( $content_type ) ) {
×
765
                        return false;
×
766
                }
767

768
                $is_valid = 0 === stripos( $content_type, 'application/json' );
×
769

770
                /**
771
                 * Allow graphql to validate custom content types for HTTP POST requests
772
                 *
773
                 * @param bool $is_valid Whether the content type is valid
774
                 * @param string $content_type The content type header value that was received
775
                 *
776
                 * @since 2.1.0
777
                 */
778
                return (bool) apply_filters( 'graphql_is_valid_http_content_type', $is_valid, $content_type );
×
779
        }
780

781
        /**
782
         * Gets the content type from the request headers
783
         */
784
        private function get_content_type(): string {
×
785
                if ( isset( $_SERVER['CONTENT_TYPE'] ) ) {
×
786
                        return sanitize_text_field( $_SERVER['CONTENT_TYPE'] );
×
787
                }
788

789
                if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
×
790
                        return sanitize_text_field( $_SERVER['HTTP_CONTENT_TYPE'] );
×
791
                }
792

793
                return '';
×
794
        }
795

796
        /**
797
         * Returns the error response for invalid content type
798
         *
799
         * @return array{errors: array{array{message: string}}}
800
         */
801
        private function get_invalid_content_type_response(): array {
×
802
                $content_type = $this->get_content_type();
×
803

804
                /**
805
                 * Filter the status code to return when the content type is invalid
806
                 *
807
                 * @param int    $status_code The status code to return. Default 415.
808
                 * @param string $content_type The content type header value that was received.
809
                 */
810
                $filtered_status_code = apply_filters( 'graphql_invalid_content_type_status_code', 415, $content_type );
×
811

812
                // Set the status code to the filtered value if it's a valid status code.
813
                if ( is_numeric( $filtered_status_code ) ) {
×
814
                        $filtered_status_code = (int) $filtered_status_code;
×
815

816
                        if ( $filtered_status_code > 100 && $filtered_status_code < 599 ) {
×
817
                                Router::$http_status_code = $filtered_status_code;
×
818
                        }
819
                }
820

821
                return [
×
822
                        'errors' => [
×
823
                                [
×
824
                                        // translators: %s is the content type header value that was received
825
                                        'message' => sprintf( esc_html__( 'HTTP POST requests must have Content-Type: application/json header. Received: %s', 'wp-graphql' ), $content_type ),
×
826
                                ],
×
827
                        ],
×
828
                ];
×
829
        }
830

831
        /**
832
         * Get the operation params for the request.
833
         *
834
         * @return \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[]
835
         */
836
        public function get_params() {
3✔
837
                return $this->params;
3✔
838
        }
839

840
        /**
841
         * Returns the debug flag value
842
         *
843
         * @return int
844
         */
845
        public function get_debug_flag() {
793✔
846
                $flag = DebugFlag::INCLUDE_DEBUG_MESSAGE;
793✔
847
                if ( 0 !== get_current_user_id() ) {
793✔
848
                        // Flag 2 shows the trace data, which should require user to be logged in to see by default
849
                        $flag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
293✔
850
                }
851

852
                return true === \WPGraphQL::debug() ? $flag : DebugFlag::NONE;
793✔
853
        }
854

855
        /**
856
         * Determines if batch queries are enabled for the server.
857
         *
858
         * Default is to have batch queries enabled.
859
         */
860
        private function is_batch_queries_enabled(): bool {
×
861
                $batch_queries_enabled = true;
×
862

863
                $batch_queries_setting = get_graphql_setting( 'batch_queries_enabled', 'on' );
×
864
                if ( 'off' === $batch_queries_setting ) {
×
865
                        $batch_queries_enabled = false;
×
866
                }
867

868
                /**
869
                 * Filter whether batch queries are supported or not
870
                 *
871
                 * @param bool                                                              $batch_queries_enabled Whether Batch Queries should be enabled
872
                 * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params Request operation params
873
                 */
874
                return (bool) apply_filters( 'graphql_is_batch_queries_enabled', $batch_queries_enabled, $this->params );
×
875
        }
876

877
        /**
878
         * Create the GraphQL server that will process the request.
879
         */
880
        private function get_server(): StandardServer {
×
881
                $debug_flag = $this->get_debug_flag();
×
882

883
                $config = new ServerConfig();
×
884
                $config
×
885
                        ->setDebugFlag( $debug_flag )
×
886
                        ->setSchema( $this->schema )
×
887
                        ->setContext( $this->app_context )
×
888
                        ->setValidationRules( $this->validation_rules )
×
889
                        ->setQueryBatching( $this->is_batch_queries_enabled() );
×
890

891
                if ( ! empty( $this->get_root_value() ) ) {
×
892
                        $config->setRootValue( $this->get_root_value() );
×
893
                }
894

895
                if ( ! empty( $this->field_resolver ) ) {
×
896
                        $config->setFieldResolver( $this->field_resolver );
×
897
                }
898

899
                /**
900
                 * Run an action when the server config is created. The config can be acted
901
                 * upon directly to override default values or implement new features, e.g.,
902
                 * $config->setValidationRules().
903
                 *
904
                 * @param \GraphQL\Server\ServerConfig                                      $config Server config
905
                 * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params Request operation params
906
                 *
907
                 * @since 0.2.0
908
                 */
909
                do_action( 'graphql_server_config', $config, $this->params );
×
910

911
                return new StandardServer( $config );
×
912
        }
913
}
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