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

wp-graphql / wp-graphql-woocommerce / 23228201207

18 Mar 2026 03:49AM UTC coverage: 83.184% (-0.4%) from 83.59%
23228201207

push

github

web-flow
fix: inverted logic in pop_transaction_id() causes cart session corruption (#971)

* fix: resolve REQUEST_URI fatal error and JWT key length issues in CI

QLSessionHandlerTest::tearDown() was calling unset($_SERVER) which
destroyed the entire superglobal. WordPress cron.php then fataled on
shutdown when accessing $_SERVER['REQUEST_URI']. Changed to only unset
the specific HTTP_WOOCOMMERCE_SESSION key.

Also updated JWT secret keys to meet firebase/php-jwt v7's minimum
32-byte requirement for HS256 in both test config and Docker entrypoint.

* fix: the rest of the files added

* devops: php7.4 removed from matrix

* chore: Linter compliances met

* devops: More broken test updated

* devops: Tests updated for CI

* fix: QLSessionHandlerCest fixed

* fix: QLSessionHandlerCest fixed

* devops: CI fixed

3 of 59 new or added lines in 2 files covered. (5.08%)

526 existing lines in 17 files now uncovered.

12594 of 15140 relevant lines covered (83.18%)

75.9 hits per line

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

59.49
/includes/utils/class-ql-session-handler.php
1
<?php
2
/**
3
 * Handles data for the current customers session.
4
 *
5
 * @package WPGraphQL\WooCommerce\Utils
6
 * @since 0.1.2
7
 */
8

9
namespace WPGraphQL\WooCommerce\Utils;
10

11
use WC_Session_Handler;
12
use WPGraphQL\Router;
13
use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\JWT;
14
use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\Key;
15

16
/**
17
 * Class - QL_Session_Handler
18
 *
19
 * @property int $_session_expiring
20
 * @property int $_session_expiration
21
 * @property int|string $_customer_id
22
 */
23
class QL_Session_Handler extends WC_Session_Handler {
24
        /**
25
         * Stores the name of the HTTP header used to pass the session token.
26
         *
27
         * @var string $_token
28
         */
29
        protected $_token; // @codingStandardsIgnoreLine
30

31
        /**
32
         * Stores Timestamp of when the session token was issued.
33
         *
34
         * @var float $_session_issued
35
         */
36
        protected $_session_issued; // @codingStandardsIgnoreLine
37

38
        /**
39
         * True when the token exists.
40
         *
41
         * @var bool $_has_token
42
         */
43
        protected $_has_token = false; // @codingStandardsIgnoreLine
44

45
        /**
46
         * True when a new session token has been issued.
47
         *
48
         * @var bool $_issuing_new_token
49
         */
50
        protected $_issuing_new_token = false; // @codingStandardsIgnoreLine
51

52
        /**
53
         * True when a new session cookie has been issued.
54
         *
55
         * @var bool $_issuing_new_cookie
56
         */
57
        protected $_issuing_new_cookie = false; // @codingStandardsIgnoreLine
58

59
        /**
60
         * Constructor for the session class.
61
         */
62
        public function __construct() {
63
                parent::__construct();
11✔
64

65
                $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' );
11✔
66
        }
67

68
        /**
69
         * Returns formatted $_SERVER index from provided string.
70
         *
71
         * @param string $header String to be formatted.
72
         *
73
         * @return string
74
         */
75
        private function get_server_key( $header = null ) {
76
                /**
77
                 * Server key.
78
                 *
79
                 * @var string $server_key
80
                 */
81
                $server_key = preg_replace( '#[^A-z0-9]#', '_', ! empty( $header ) ? $header : $this->_token );
59✔
82
                return null !== $server_key
59✔
83
                        ? 'HTTP_' . strtoupper( $server_key )
59✔
84
                        : '';
59✔
85
        }
86

87
        /**
88
         * This returns the secret key, using the defined constant if defined, and passing it through a filter to
89
         * allow for the config to be able to be set via another method other than a defined constant, such as an
90
         * admin UI that allows the key to be updated/changed/revoked at any time without touching server files
91
         *
92
         * @return mixed|null|string
93
         */
94
        private function get_secret_key() {
95
                // Use the defined secret key, if it exists.
96
                $secret_key = defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) && GRAPHQL_WOOCOMMERCE_SECRET_KEY !== false && GRAPHQL_WOOCOMMERCE_SECRET_KEY !== ''
6✔
97
                        ? GRAPHQL_WOOCOMMERCE_SECRET_KEY :
6✔
98
                        'graphql-woo-cart-session';
×
99
                return apply_filters( 'graphql_woocommerce_secret_key', $secret_key );
6✔
100
        }
101

102
        /**
103
         * Init hooks and session data.
104
         *
105
         * @return void
106
         */
107
        public function init() {
108
                $this->init_session_token();
54✔
109
                Session_Transaction_Manager::get( $this );
54✔
110

111
                /**
112
                 * Necessary since Session_Transaction_Manager applies to the reference.
113
                 *
114
                 * @var self $this
115
                 */
116
                if ( Router::is_graphql_http_request() ) {
54✔
117
                        add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 );
2✔
118
                        add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 );
2✔
119
                        add_action( 'shutdown', [ $this, 'save_data' ] );
2✔
120
                        add_filter( 'graphql_jwt_auth_after_authenticate', [ $this, 'reinitialize_session_token' ], 10 );
2✔
121
                        add_filter( 'graphql_login_payload', [ $this, 'reinitialize_session_token' ], 10 );
2✔
122
                } else {
123
                        add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_cookie' ], 10 );
53✔
124
                        add_action( 'shutdown', [ $this, 'save_data' ], 20 );
53✔
125
                        add_action( 'wp_logout', [ $this, 'destroy_session' ] );
53✔
126
                        if ( ! is_user_logged_in() ) {
53✔
127
                                add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 );
3✔
128
                        }
129
                }
130
        }
131

132
        /**
133
         * Mark the session as dirty.
134
         *
135
         * To trigger a save of the session data.
136
         *
137
         * @return void
138
         */
139
        public function mark_dirty() {
140
                $this->_dirty = true;
1✔
141
        }
142

143
        /**
144
         * Setup token and customer ID.
145
         *
146
         * @throws \GraphQL\Error\UserError Invalid token.
147
         *
148
         * @return void
149
         */
150
        public function init_session_token() {
151

152
                /**
153
                 * @var object{ iat: int, exp: int, data: object{ customer_id: string } }|false|\WP_Error $token
154
                 */
155
                $token = $this->get_session_token();
59✔
156

157
                // Process existing session if not expired or invalid.
158
                if ( $token && is_object( $token ) && ! is_wp_error( $token ) ) {
59✔
159
                        $this->_customer_id        = $token->data->customer_id;
2✔
160
                        $this->_session_issued     = $token->iat;
2✔
161
                        $this->_session_expiration = $token->exp;
2✔
162
                        $this->_session_expiring   = $token->exp - ( 3600 );
2✔
163
                        $this->_has_token          = true;
2✔
164
                        $this->_data               = $this->get_session_data();
2✔
165

166
                        // If the user logs in, update session.
167
                        if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) {
2✔
168
                                $guest_session_id   = $this->_customer_id;
×
UNCOV
169
                                $this->_customer_id = strval( get_current_user_id() );
×
UNCOV
170
                                $this->_dirty       = true;
×
171

172
                                // If session empty check for previous data associated with customer and assign that to the session.
UNCOV
173
                                if ( empty( $this->_data ) ) {
×
UNCOV
174
                                        $this->_data = $this->get_session_data();
×
175
                                }
176

177
                                // @phpstan-ignore-next-line
178
                                $this->save_data( $guest_session_id );
×
179
                                Router::is_graphql_http_request()
×
UNCOV
180
                                        ? $this->set_customer_session_token( true )
×
UNCOV
181
                                        : $this->set_customer_session_cookie( true );
×
182
                        }
183

184
                        // Update session expiration on each action.
185
                        $this->set_session_expiration();
2✔
186
                        if ( $token->exp < $this->_session_expiration ) {
2✔
187
                                $this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration );
1✔
188
                        }
189
                } elseif ( Router::is_graphql_http_request() && is_wp_error( $token ) ) {
59✔
190
                        add_filter(
×
191
                                'graphql_woocommerce_session_token_errors',
×
192
                                static function ( $errors ) use ( $token ) {
×
193
                                        $errors = $token->get_error_code() . ': ' . $token->get_error_message();
×
194
                                        return $errors;
×
UNCOV
195
                                }
×
UNCOV
196
                        );
×
197
                }
198

199
                $start_new_session = ! $token || is_wp_error( $token );
59✔
200
                if ( ! $start_new_session ) {
59✔
201
                        return;
2✔
202
                }
203

204
                // Distribute new session token on GraphQL requests, otherwise distribute a new session cookie.
205
                if ( Router::is_graphql_http_request() ) {
59✔
206
                        // Start new session.
207
                        $this->set_session_expiration();
6✔
208

209
                        // Get Customer ID.
210
                        $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
6✔
211
                        $this->_data        = $this->get_session_data();
6✔
212
                        $this->set_customer_session_token( true );
6✔
213
                } else {
214
                        $this->init_session_cookie();
53✔
215
                }
216
        }
217

218
        /**
219
         * Reinitialize session token in response after authentication in GraphQL.
220
         *
221
         * @param array $response The authentication response.
222
         *
223
         * @return array
224
         */
225
        public function reinitialize_session_token( $response ) {
UNCOV
226
                $this->init_session_token();
×
227

UNCOV
228
                $token = $this->build_token();
×
UNCOV
229
                if ( $token ) {
×
UNCOV
230
                        $response['session_token'] = $token;
×
231
                }
232

233
                // Add Store API Cart-Token if enabled.
234
                $cart_token = $this->build_cart_token();
×
235
                if ( ! empty( $cart_token ) ) {
×
UNCOV
236
                        $response['cart_token'] = $cart_token;
×
237
                }
238
                return $response;
×
239
        }
240

241
        /**
242
         * Retrieve and decrypt the session data from session, if set. Otherwise return false.
243
         *
244
         * Session cookies without a customer ID are invalid.
245
         *
246
         * @throws \Exception  Invalid token.
247
         * @return false|\WP_Error|object{ iat: int, exp: int, data: object{ customer_id: string } }
248
         */
249
        public function get_session_token() {
250
                // Get the Auth header.
251
                $session_header = $this->get_session_header();
59✔
252

253
                if ( empty( $session_header ) ) {
59✔
254
                        return false;
59✔
255
                }
256

257
                // Determine token type by checking for "Session " prefix.
258
                $is_legacy_token = 0 === strpos( $session_header, 'Session ' );
3✔
259

260
                if ( $is_legacy_token ) {
3✔
261
                        return $this->validate_legacy_token( $session_header );
3✔
262
                }
263

UNCOV
264
                return $this->validate_cart_token( $session_header );
×
265
        }
266

267
        /**
268
         * Validate legacy GraphQL session token
269
         *
270
         * @param string $session_header The session header value.
271
         *
272
         * @throws \Exception Invalid token.
273
         *
274
         * @return object{ iat: int, exp: int, data: object{ customer_id: string } }|\WP_Error|false
275
         */
276
        protected function validate_legacy_token( $session_header ) {
277
                // Get the token from the header.
278
                $token_string = sscanf( $session_header, 'Session %s' );
3✔
279
                if ( empty( $token_string ) ) {
3✔
UNCOV
280
                        return false;
×
281
                }
282

283
                list( $token ) = $token_string;
3✔
284

285
                /**
286
                 * Try to decode the token
287
                 */
288
                try {
289
                        JWT::$leeway = 60;
3✔
290

291
                        $secret = $this->get_secret_key();
3✔
292
                        $key    = new Key( $secret, 'HS256' );
3✔
293
                        /**
294
                         * Decode the token
295
                         *
296
                         * @var null|object{ iat: int, exp: int, data: object{ customer_id: string }, iss: string } $token
297
                         */
298
                        $token = ! empty( $token ) ? JWT::decode( $token, $key ) : null;
3✔
299

300
                        // Check if token was successful decoded.
301
                        if ( ! $token ) {
3✔
UNCOV
302
                                throw new \Exception( __( 'Failed to decode session token', 'wp-graphql-woocommerce' ) );
×
303
                        }
304

305
                        // The Token is decoded now validate the iss.
306
                        if ( empty( $token->iss ) || get_bloginfo( 'url' ) !== $token->iss ) {
3✔
UNCOV
307
                                throw new \Exception( __( 'The iss do not match with this server', 'wp-graphql-woocommerce' ) );
×
308
                        }
309

310
                        // Validate the customer id in the token.
311
                        if ( empty( $token->data ) || empty( $token->data->customer_id ) ) {
3✔
312
                                throw new \Exception( __( 'Customer ID not found in the token', 'wp-graphql-woocommerce' ) );
3✔
313
                        }
UNCOV
314
                } catch ( \Throwable $error ) {
×
UNCOV
315
                        return new \WP_Error( 'invalid_token', $error->getMessage() );
×
316
                }//end try
317

318
                return $token;
3✔
319
        }
320

321
        /**
322
         * Validate Store API Cart-Token
323
         *
324
         * @param string $cart_token The Cart-Token value.
325
         *
326
         * @throws \Exception Invalid token.
327
         *
328
         * @return object{ iat: int, exp: int, data: object{ customer_id: string } }|\WP_Error|false
329
         */
330
        protected function validate_cart_token( $cart_token ) {
331
                // Validate Cart-Token using WooCommerce's JsonWebToken utility if available.
332
                if ( ! $this->supports_store_api() ) {
×
333
                        return new \WP_Error( 'store_api_not_supported', __( 'Store API not available', 'wp-graphql-woocommerce' ) );
×
334
                }
335

336
                try {
337
                        $secret   = '@' . wp_salt();
×
UNCOV
338
                        $is_valid = \Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken::validate(
×
UNCOV
339
                                $cart_token,
×
340
                                $secret
×
341
                        );
×
342

343
                        if ( ! $is_valid ) {
×
344
                                throw new \Exception( __( 'Invalid Cart-Token', 'wp-graphql-woocommerce' ) );
×
345
                        }
346

347
                        // Decode the token to get the payload.
348
                        /** @var object{ payload: object{ user_id: string, iat: int, exp: int } } $parts */
UNCOV
349
                        $parts = \Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken::get_parts( $cart_token );
×
350

351
                        // Transform to match legacy token structure for compatibility.
352
                        /** @var object{ iat: int, exp: int, data: object{ customer_id: string } } $payload */
UNCOV
353
                        $payload = (object) [
×
UNCOV
354
                                'iat'  => $parts->payload->iat,
×
UNCOV
355
                                'exp'  => $parts->payload->exp,
×
UNCOV
356
                                'data' => (object) [ 'customer_id' => $parts->payload->user_id ?? '' ],
×
357
                        ];
×
358
                } catch ( \Throwable $error ) {
×
359
                        return new \WP_Error( 'invalid_cart_token', $error->getMessage() );
×
360
                }//end try
361

362
                return $payload;
×
363
        }
364

365
        /**
366
         * Get the value of the cart session header from the $_SERVER super global
367
         *
368
         * @return mixed|string
369
         */
370
        public function get_session_header() {
371
                $token_type = woographql_setting( 'set_session_token_type', 'legacy' );
59✔
372

373
                // Check for Cart-Token header first if Store API mode is enabled.
374
                if ( in_array( $token_type, [ 'store-api', 'both' ], true ) && isset( $_SERVER['HTTP_CART_TOKEN'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
59✔
UNCOV
375
                        $cart_token = $_SERVER['HTTP_CART_TOKEN']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
×
376

377
                        /**
378
                         * Return the cart session header, passed through a filter
379
                         *
380
                         * @param string $session_header  The header used to identify a user's cart session token.
381
                         */
382
                        return apply_filters( 'graphql_woocommerce_cart_session_header', $cart_token );
×
383
                }
384

385
                // Fall back to legacy woocommerce-session header.
386
                $session_header_key = $this->get_server_key();
59✔
387

388
                // Looking for the cart session header.
389
                $session_header = isset( $_SERVER[ $session_header_key ] )
59✔
390
                        ? $_SERVER[ $session_header_key ] //@codingStandardsIgnoreLine
4✔
391
                        : false;
59✔
392

393
                /**
394
                 * Return the cart session header, passed through a filter
395
                 *
396
                 * @param string $session_header  The header used to identify a user's cart session token.
397
                 */
398
                return apply_filters( 'graphql_woocommerce_cart_session_header', $session_header );
59✔
399
        }
400

401
        /**
402
         * Determine if a JWT is being sent in the page response.
403
         *
404
         * @return bool
405
         */
406
        public function sending_token() {
407
                return $this->_has_token || $this->_issuing_new_token;
8✔
408
        }
409

410
        /**
411
         * Determine if a HTTP cookie is being sent in the page response.
412
         *
413
         * @return bool
414
         */
415
        public function sending_cookie() {
UNCOV
416
                return $this->_has_cookie || $this->_issuing_new_cookie;
×
417
        }
418

419
        /**
420
         * Creates JSON Web Token for customer session.
421
         *
422
         * @return false|string
423
         */
424
        public function build_token() {
425
                if ( empty( $this->_session_issued ) || ! $this->sending_token() ) {
8✔
426
                        return false;
3✔
427
                }
428

429
                // Check if legacy GraphQL token generation is enabled.
430
                $token_type = woographql_setting( 'set_session_token_type', 'legacy' );
6✔
431
                if ( ! in_array( $token_type, [ 'legacy', 'both' ], true ) ) {
6✔
UNCOV
432
                        return false;
×
433
                }
434

435
                /**
436
                 * Determine the "not before" value for use in the token
437
                 *
438
                 * @param float      $issued        The timestamp of token was issued.
439
                 * @param int|string $customer_id   Customer ID.
440
                 * @param array      $session_data  Cart session data.
441
                 */
442
                $not_before = apply_filters(
6✔
443
                        'graphql_woo_cart_session_not_before',
6✔
444
                        $this->_session_issued,
6✔
445
                        $this->_customer_id,
6✔
446
                        $this->_data
6✔
447
                );
6✔
448

449
                // Configure the token array, which will be encoded.
450
                $token = [
6✔
451
                        'iss'  => get_bloginfo( 'url' ),
6✔
452
                        'iat'  => $this->_session_issued,
6✔
453
                        'nbf'  => $not_before,
6✔
454
                        'exp'  => $this->_session_expiration,
6✔
455
                        'data' => [
6✔
456
                                'customer_id' => $this->_customer_id,
6✔
457
                        ],
6✔
458
                ];
6✔
459

460
                /**
461
                 * Filter the token, allowing for individual systems to configure the token as needed
462
                 *
463
                 * @param array      $token         The token array that will be encoded
464
                 * @param int|string $customer_id   ID of customer associated with token.
465
                 * @param array      $session_data  Session data associated with token.
466
                 */
467
                $token = apply_filters(
6✔
468
                        'graphql_woocommerce_cart_session_before_token_sign',
6✔
469
                        $token,
6✔
470
                        $this->_customer_id,
6✔
471
                        $this->_data
6✔
472
                );
6✔
473

474
                // Encode the token.
475
                JWT::$leeway = 60;
6✔
476
                $token       = JWT::encode( $token, $this->get_secret_key(), 'HS256' );
6✔
477

478
                /**
479
                 * Filter the token before returning it, allowing for individual systems to override what's returned.
480
                 *
481
                 * For example, if the user should not be granted a token for whatever reason, a filter could have the token return null.
482
                 *
483
                 * @param string     $token         The signed JWT token that will be returned
484
                 * @param int|string $customer_id   ID of customer associated with token.
485
                 * @param array      $session_data  Session data associated with token.
486
                 */
487
                $token = apply_filters(
6✔
488
                        'graphql_woocommerce_cart_session_signed_token',
6✔
489
                        $token,
6✔
490
                        $this->_customer_id,
6✔
491
                        $this->_data
6✔
492
                );
6✔
493

494
                return $token;
6✔
495
        }
496

497
        /**
498
         * Build a Store API compatible Cart-Token JWT.
499
         *
500
         * Generates a JWT token compatible with WooCommerce Store API (used by WooCommerce Blocks).
501
         * This enables session sharing between GraphQL mutations and WooCommerce Blocks cart/checkout.
502
         *
503
         * @since 0.22.0
504
         *
505
         * @return string|null Cart-Token JWT or null if feature disabled or unavailable.
506
         */
507
        public function build_cart_token() {
508
                // Check if Store API token generation is enabled.
509
                $token_type = woographql_setting( 'set_session_token_type', 'legacy' );
1✔
510
                if ( ! in_array( $token_type, [ 'store-api', 'both' ], true ) ) {
1✔
511
                        return null;
1✔
512
                }
513

514
                // Ensure session is active.
UNCOV
515
                if ( empty( $this->_session_issued ) || ! $this->sending_token() ) {
×
UNCOV
516
                        return null;
×
517
                }
518

519
                // Check if WooCommerce Store API utilities are available.
UNCOV
520
                if ( ! $this->supports_store_api() ) {
×
UNCOV
521
                        return null;
×
522
                }
523

524
                // Generate Cart-Token using WooCommerce's Store API pattern.
525
                try {
UNCOV
526
                        $token = \Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken::create(
×
UNCOV
527
                                [
×
UNCOV
528
                                        'user_id' => $this->_customer_id,
×
UNCOV
529
                                        'exp'     => $this->_session_expiration,
×
UNCOV
530
                                        'iss'     => 'store-api',
×
UNCOV
531
                                ],
×
UNCOV
532
                                '@' . wp_salt()
×
UNCOV
533
                        );
×
534

535
                        /**
536
                         * Filter the Store API Cart-Token before returning.
537
                         *
538
                         * @since 0.22.0
539
                         *
540
                         * @param string     $token         The signed Cart-Token JWT
541
                         * @param int|string $customer_id   ID of customer associated with token
542
                         * @param array      $session_data  Session data associated with token
543
                         */
UNCOV
544
                        $token = apply_filters(
×
UNCOV
545
                                'graphql_woocommerce_store_api_cart_token',
×
UNCOV
546
                                $token,
×
UNCOV
547
                                $this->_customer_id,
×
UNCOV
548
                                $this->_data
×
UNCOV
549
                        );
×
550

UNCOV
551
                        return $token;
×
UNCOV
552
                } catch ( \Throwable $e ) {
×
553
                        // Log error but don't break GraphQL response.
UNCOV
554
                        do_action( 'graphql_debug', sprintf( 'Failed to generate Cart-Token: %s', $e->getMessage() ) );
×
UNCOV
555
                        return null;
×
556
                }
557
        }
558

559
        /**
560
         * Check if WooCommerce version supports Store API.
561
         *
562
         * Store API Cart-Token functionality requires WooCommerce 5.5.0+.
563
         *
564
         * @since 0.22.0
565
         *
566
         * @return bool
567
         */
568
        protected function supports_store_api() {
569
                // Check WooCommerce is active.
UNCOV
570
                if ( ! defined( 'WC_VERSION' ) ) {
×
UNCOV
571
                        return false;
×
572
                }
573

574
                // Store API CartTokenUtils introduced in WC 5.5.0.
UNCOV
575
                if ( version_compare( WC_VERSION, '5.5.0', '<' ) ) {
×
UNCOV
576
                        return false;
×
577
                }
578

579
                // Check if Store API JWT class is available.
UNCOV
580
                if ( ! class_exists( '\Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken' ) ) {
×
UNCOV
581
                        return false;
×
582
                }
583

UNCOV
584
                return true;
×
585
        }
586

587
        /**
588
         * Sets the session header on-demand (usually after adding an item to the cart).
589
         *
590
         * Warning: Headers will only be set if this is called before the headers are sent.
591
         *
592
         * @param bool $set Should the session cookie be set.
593
         *
594
         * @return void
595
         */
596
        public function set_customer_session_token( $set ) {
597
                if ( ! empty( $this->_session_issued ) && $set ) {
6✔
598
                        /**
599
                         * Set callback session token(s) for use in the HTTP response headers.
600
                         * Depending on the session token type setting, this may send:
601
                         * - Legacy GraphQL session token (woocommerce-session header)
602
                         * - Store API Cart-Token header
603
                         * - Both headers
604
                         */
605
                        add_filter(
6✔
606
                                'graphql_response_headers_to_send',
6✔
607
                                function ( $headers ) {
6✔
608
                                        // Add legacy GraphQL session token if enabled.
609
                                        $token = $this->build_token();
1✔
610
                                        if ( $token ) {
1✔
611
                                                $headers[ $this->_token ] = $token;
1✔
612
                                        }
613

614
                                        // Add Store API Cart-Token if enabled.
615
                                        $cart_token = $this->build_cart_token();
1✔
616
                                        if ( ! empty( $cart_token ) ) {
1✔
UNCOV
617
                                                $headers['Cart-Token'] = $cart_token;
×
618
                                        }
619

620
                                        return $headers;
1✔
621
                                },
6✔
622
                                10
6✔
623
                        );
6✔
624

625
                        $this->_issuing_new_token = true;
6✔
626
                }
627
        }
628

629
        /**
630
         * {@inheritDoc}
631
         *
632
         * @return void
633
         */
634
        public function set_customer_session_cookie( $set ) {
UNCOV
635
                parent::set_customer_session_cookie( $set );
×
636

UNCOV
637
                if ( $set ) {
×
UNCOV
638
                        $this->_issuing_new_cookie = true;
×
639
                }
640
        }
641

642
        /**
643
         * Return true if the current user has an active session, i.e. a cookie to retrieve values.
644
         *
645
         * @return bool
646
         */
647
        public function has_session() {
648

649
                // @codingStandardsIgnoreLine.
650
                return $this->_issuing_new_token || $this->_has_token || parent::has_session();
117✔
651
        }
652

653
        /**
654
         * Set session expiration.
655
         *
656
         * @return void
657
         */
658
        public function set_session_expiration() {
659
                $this->_session_issued = time();
11✔
660

661
                parent::set_session_expiration();
11✔
662

663
                $this->_session_expiration = apply_filters_deprecated(
11✔
664
                        'graphql_woocommerce_cart_session_expire',
11✔
665
                        [ $this->_session_expiration ],
11✔
666
                        '0.21.0',
11✔
667
                        'wc_session_expiration'
11✔
668
                );
11✔
669
        }
670

671
        /**
672
         * Save any changes to database after a session mutations has been run.
673
         *
674
         * @return void
675
         */
676
        public function save_if_dirty() {
677
                // Update if user recently authenticated.
UNCOV
678
                if ( is_user_logged_in() && get_current_user_id() !== $this->_customer_id ) {
×
UNCOV
679
                        $this->_customer_id = get_current_user_id();
×
UNCOV
680
                        $this->_dirty       = true;
×
681
                }
682

683
                // Bail if no changes.
UNCOV
684
                if ( ! $this->_dirty ) {
×
UNCOV
685
                        return;
×
686
                }
687

UNCOV
688
                $this->save_data();
×
689
        }
690

691
        /**
692
         * For refreshing session data mid-request when changes occur in concurrent requests.
693
         *
694
         * @return void
695
         */
696
        public function reload_data() {
UNCOV
697
                \WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
×
698

699
                // Get session data.
UNCOV
700
                $data = $this->get_session( (string) $this->_customer_id );
×
UNCOV
701
                if ( is_array( $data ) ) {
×
UNCOV
702
                        $this->_data = $data;
×
703
                }
704
        }
705

706
        /**
707
         * Returns "client_session_id". "client_session_id_expiration" is used
708
         * to keep "client_session_id" as fresh as possible.
709
         *
710
         * For the most strict level of security it's highly recommend these values
711
         * be set client-side using the `updateSession` mutation.
712
         * "client_session_id" in particular should be salted with some
713
         * kind of client identifier like the end-user "IP" or "user-agent"
714
         * then hashed parodying the tokens generated by
715
         * WP's WP_Session_Tokens class.
716
         *
717
         * @return string
718
         */
719
        public function get_client_session_id() {
720
                // Get client session ID.
721
                $client_session_id            = $this->get( 'client_session_id', false );
2✔
722
                $client_session_id_expiration = absint( $this->get( 'client_session_id_expiration', 0 ) );
2✔
723

724
                // If client session ID valid return it.
725
                if ( false !== $client_session_id && time() < $client_session_id_expiration ) {
2✔
726
                        // @phpstan-ignore-next-line
727
                        return $client_session_id;
2✔
728
                }
729

730
                // Generate a new client session ID.
731
                $client_session_id            = uniqid();
2✔
732
                $client_session_id_expiration = time() + 3600;
2✔
733
                $this->set( 'client_session_id', $client_session_id );
2✔
734
                $this->set( 'client_session_id_expiration', $client_session_id_expiration );
2✔
735
                $this->save_data();
2✔
736

737
                // Return new client session ID.
738
                return $client_session_id;
2✔
739
        }
740
}
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