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

wp-graphql / wp-graphql-woocommerce / 23675310087

28 Mar 2026 02:18AM UTC coverage: 89.672% (+0.2%) from 89.424%
23675310087

Pull #1003

github

web-flow
Merge fb368619b into 6fb7b226f
Pull Request #1003: devops: WC email template tests, COT cursor HPOS fix, checkout account auth

75 of 81 new or added lines in 5 files covered. (92.59%)

46 existing lines in 8 files now uncovered.

15915 of 17748 relevant lines covered (89.67%)

143.3 hits per line

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

84.21
/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();
38✔
64

65
                $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' );
38✔
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 );
143✔
82
                return null !== $server_key
143✔
83
                        ? 'HTTP_' . strtoupper( $server_key )
143✔
84
                        : '';
143✔
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 !== ''
32✔
97
                        ? GRAPHQL_WOOCOMMERCE_SECRET_KEY :
32✔
98
                        'graphql-woo-cart-session';
×
99
                return apply_filters( 'graphql_woocommerce_secret_key', $secret_key );
32✔
100
        }
101

102
        /**
103
         * Init hooks and session data.
104
         *
105
         * @return void
106
         */
107
        public function init() {
108
                $this->init_session_token();
137✔
109
                Session_Transaction_Manager::get( $this );
137✔
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() ) {
137✔
117
                        add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 );
29✔
118
                        add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 );
29✔
119
                        add_action( 'shutdown', [ $this, 'save_data' ] );
29✔
120
                        add_filter( 'graphql_jwt_auth_after_authenticate', [ $this, 'reinitialize_session_token' ], 10 );
29✔
121
                        add_filter( 'graphql_login_payload', [ $this, 'reinitialize_session_token' ], 10 );
29✔
122
                } else {
123
                        add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_cookie' ], 10 );
109✔
124
                        add_action( 'shutdown', [ $this, 'save_data' ], 20 );
109✔
125
                        add_action( 'wp_logout', [ $this, 'destroy_session' ] );
109✔
126
                        if ( ! is_user_logged_in() ) {
109✔
127
                                add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 );
5✔
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;
23✔
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();
143✔
156

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

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

173
                                $existing_user_data = $this->get_session_data();
13✔
174

175
                                $transfer_behavior = woographql_setting( 'session_transfer_behavior', 'keep_new_fallback_old' );
13✔
176
                                switch ( $transfer_behavior ) {
177
                                        case 'keep_new':
13✔
178
                                                $this->_data = $guest_data;
1✔
179
                                                break;
1✔
180
                                        case 'keep_old':
12✔
181
                                                $this->_data = ! empty( $existing_user_data ) ? $existing_user_data : $guest_data;
1✔
182
                                                break;
1✔
183
                                        case 'keep_new_fallback_old':
11✔
184
                                        default:
185
                                                $this->_data = ! empty( $guest_data ) ? $guest_data : $existing_user_data;
11✔
186
                                                break;
11✔
187
                                }
188

189
                                // @phpstan-ignore-next-line
190
                                $this->save_data( $guest_session_id );
13✔
191
                                Router::is_graphql_http_request()
13✔
192
                                        ? $this->set_customer_session_token( true )
13✔
193
                                        : $this->set_customer_session_cookie( true );
×
194
                        }
195

196
                        // Update session expiration on each action.
197
                        $this->set_session_expiration();
21✔
198
                        if ( $token->exp < $this->_session_expiration ) {
21✔
199
                                $this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration );
20✔
200
                        }
201
                } elseif ( Router::is_graphql_http_request() && is_wp_error( $token ) ) {
143✔
202
                        add_filter(
1✔
203
                                'graphql_woocommerce_session_token_errors',
1✔
204
                                static function ( $errors ) use ( $token ) {
1✔
205
                                        $errors = $token->get_error_code() . ': ' . $token->get_error_message();
1✔
206
                                        return $errors;
1✔
207
                                }
1✔
208
                        );
1✔
209
                }
210

211
                $start_new_session = ! $token || is_wp_error( $token );
143✔
212
                if ( ! $start_new_session ) {
143✔
213
                        return;
21✔
214
                }
215

216
                // Distribute new session token on GraphQL requests, otherwise distribute a new session cookie.
217
                if ( Router::is_graphql_http_request() ) {
143✔
218
                        // Start new session.
219
                        $this->set_session_expiration();
33✔
220

221
                        // Get Customer ID.
222
                        $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
33✔
223
                        $this->_data        = $this->get_session_data();
33✔
224
                        $this->set_customer_session_token( true );
33✔
225
                } else {
226
                        $this->init_session_cookie();
110✔
227
                }
228
        }
229

230
        /**
231
         * Reinitialize session token in response after authentication in GraphQL.
232
         *
233
         * @param array $response The authentication response.
234
         *
235
         * @return array
236
         */
237
        public function reinitialize_session_token( $response ) {
238
                $this->init_session_token();
15✔
239

240
                $token = $this->build_token();
15✔
241
                if ( $token ) {
15✔
242
                        $response['session_token'] = $token;
15✔
243
                }
244

245
                // Add Store API Cart-Token if enabled.
246
                $cart_token = $this->build_cart_token();
15✔
247
                if ( ! empty( $cart_token ) ) {
15✔
248
                        $response['cart_token'] = $cart_token;
×
249
                }
250
                return $response;
15✔
251
        }
252

253
        /**
254
         * Retrieve and decrypt the session data from session, if set. Otherwise return false.
255
         *
256
         * Session cookies without a customer ID are invalid.
257
         *
258
         * @throws \Exception  Invalid token.
259
         * @return false|\WP_Error|object{ iat: int, exp: int, data: object{ customer_id: string } }
260
         */
261
        public function get_session_token() {
262
                // Get the Auth header.
263
                $session_header = $this->get_session_header();
143✔
264

265
                if ( empty( $session_header ) ) {
143✔
266
                        return false;
143✔
267
                }
268

269
                // Determine token type by checking for "Session " prefix.
270
                $is_legacy_token = 0 === strpos( $session_header, 'Session ' );
23✔
271

272
                if ( $is_legacy_token ) {
23✔
273
                        return $this->validate_legacy_token( $session_header );
23✔
274
                }
275

276
                return $this->validate_cart_token( $session_header );
×
277
        }
278

279
        /**
280
         * Validate legacy GraphQL session token
281
         *
282
         * @param string $session_header The session header value.
283
         *
284
         * @throws \Exception Invalid token.
285
         *
286
         * @return object{ iat: int, exp: int, data: object{ customer_id: string } }|\WP_Error|false
287
         */
288
        protected function validate_legacy_token( $session_header ) {
289
                // Get the token from the header.
290
                $token_string = sscanf( $session_header, 'Session %s' );
23✔
291
                if ( empty( $token_string ) ) {
23✔
292
                        return false;
×
293
                }
294

295
                list( $token ) = $token_string;
23✔
296

297
                /**
298
                 * Try to decode the token
299
                 */
300
                try {
301
                        JWT::$leeway = 60;
23✔
302

303
                        $secret = $this->get_secret_key();
23✔
304
                        $key    = new Key( $secret, 'HS256' );
23✔
305
                        /**
306
                         * Decode the token
307
                         *
308
                         * @var null|object{ iat: int, exp: int, data: object{ customer_id: string }, iss: string } $token
309
                         */
310
                        $token = ! empty( $token ) ? JWT::decode( $token, $key ) : null;
23✔
311

312
                        // Check if token was successful decoded.
313
                        if ( ! $token ) {
23✔
314
                                throw new \Exception( __( 'Failed to decode session token', 'wp-graphql-woocommerce' ) );
×
315
                        }
316

317
                        // The Token is decoded now validate the iss.
318
                        if ( empty( $token->iss ) || get_bloginfo( 'url' ) !== $token->iss ) {
23✔
319
                                throw new \Exception( __( 'The iss do not match with this server', 'wp-graphql-woocommerce' ) );
1✔
320
                        }
321

322
                        // Validate the customer id in the token.
323
                        if ( empty( $token->data ) || empty( $token->data->customer_id ) ) {
23✔
324
                                throw new \Exception( __( 'Customer ID not found in the token', 'wp-graphql-woocommerce' ) );
23✔
325
                        }
326
                } catch ( \Throwable $error ) {
1✔
327
                        return new \WP_Error( 'invalid_token', $error->getMessage() );
1✔
328
                }//end try
329

330
                return $token;
22✔
331
        }
332

333
        /**
334
         * Validate Store API Cart-Token
335
         *
336
         * @param string $cart_token The Cart-Token value.
337
         *
338
         * @throws \Exception Invalid token.
339
         *
340
         * @return object{ iat: int, exp: int, data: object{ customer_id: string } }|\WP_Error|false
341
         */
342
        protected function validate_cart_token( $cart_token ) {
343
                // Validate Cart-Token using WooCommerce's JsonWebToken utility if available.
344
                if ( ! $this->supports_store_api() ) {
×
345
                        return new \WP_Error( 'store_api_not_supported', __( 'Store API not available', 'wp-graphql-woocommerce' ) );
×
346
                }
347

348
                try {
349
                        $secret   = '@' . wp_salt();
×
350
                        $is_valid = \Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken::validate(
×
351
                                $cart_token,
×
352
                                $secret
×
353
                        );
×
354

355
                        if ( ! $is_valid ) {
×
356
                                throw new \Exception( __( 'Invalid Cart-Token', 'wp-graphql-woocommerce' ) );
×
357
                        }
358

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

363
                        // Transform to match legacy token structure for compatibility.
364
                        /** @var object{ iat: int, exp: int, data: object{ customer_id: string } } $payload */
365
                        $payload = (object) [
×
366
                                'iat'  => $parts->payload->iat,
×
367
                                'exp'  => $parts->payload->exp,
×
368
                                'data' => (object) [ 'customer_id' => $parts->payload->user_id ?? '' ],
×
369
                        ];
×
370
                } catch ( \Throwable $error ) {
×
371
                        return new \WP_Error( 'invalid_cart_token', $error->getMessage() );
×
372
                }//end try
373

374
                return $payload;
×
375
        }
376

377
        /**
378
         * Get the value of the cart session header from the $_SERVER super global
379
         *
380
         * @return mixed|string
381
         */
382
        public function get_session_header() {
383
                $token_type = woographql_setting( 'set_session_token_type', 'legacy' );
143✔
384

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

389
                        /**
390
                         * Return the cart session header, passed through a filter
391
                         *
392
                         * @param string $session_header  The header used to identify a user's cart session token.
393
                         */
394
                        return apply_filters( 'graphql_woocommerce_cart_session_header', $cart_token );
×
395
                }
396

397
                // Fall back to legacy woocommerce-session header.
398
                $session_header_key = $this->get_server_key();
143✔
399

400
                // Looking for the cart session header.
401
                $session_header = isset( $_SERVER[ $session_header_key ] )
143✔
402
                        ? $_SERVER[ $session_header_key ] //@codingStandardsIgnoreLine
24✔
403
                        : false;
143✔
404

405
                /**
406
                 * Return the cart session header, passed through a filter
407
                 *
408
                 * @param string $session_header  The header used to identify a user's cart session token.
409
                 */
410
                return apply_filters( 'graphql_woocommerce_cart_session_header', $session_header );
143✔
411
        }
412

413
        /**
414
         * Determine if a JWT is being sent in the page response.
415
         *
416
         * @return bool
417
         */
418
        public function sending_token() {
419
                return $this->_has_token || $this->_issuing_new_token;
35✔
420
        }
421

422
        /**
423
         * Determine if a HTTP cookie is being sent in the page response.
424
         *
425
         * @return bool
426
         */
427
        public function sending_cookie() {
428
                return $this->_has_cookie || $this->_issuing_new_cookie;
×
429
        }
430

431
        /**
432
         * Creates JSON Web Token for customer session.
433
         *
434
         * @return false|string
435
         */
436
        public function build_token() {
437
                if ( empty( $this->_session_issued ) || ! $this->sending_token() ) {
35✔
438
                        return false;
3✔
439
                }
440

441
                // Check if legacy GraphQL token generation is enabled.
442
                $token_type = woographql_setting( 'set_session_token_type', 'legacy' );
33✔
443
                if ( ! in_array( $token_type, [ 'legacy', 'both' ], true ) ) {
33✔
444
                        return false;
1✔
445
                }
446

447
                /**
448
                 * Determine the "not before" value for use in the token
449
                 *
450
                 * @param float      $issued        The timestamp of token was issued.
451
                 * @param int|string $customer_id   Customer ID.
452
                 * @param array      $session_data  Cart session data.
453
                 */
454
                $not_before = apply_filters(
32✔
455
                        'graphql_woo_cart_session_not_before',
32✔
456
                        $this->_session_issued,
32✔
457
                        $this->_customer_id,
32✔
458
                        $this->_data
32✔
459
                );
32✔
460

461
                // Configure the token array, which will be encoded.
462
                $token = [
32✔
463
                        'iss'  => get_bloginfo( 'url' ),
32✔
464
                        'iat'  => $this->_session_issued,
32✔
465
                        'nbf'  => $not_before,
32✔
466
                        'exp'  => $this->_session_expiration,
32✔
467
                        'data' => [
32✔
468
                                'customer_id' => $this->_customer_id,
32✔
469
                        ],
32✔
470
                ];
32✔
471

472
                /**
473
                 * Filter the token, allowing for individual systems to configure the token as needed
474
                 *
475
                 * @param array      $token         The token array that will be encoded
476
                 * @param int|string $customer_id   ID of customer associated with token.
477
                 * @param array      $session_data  Session data associated with token.
478
                 */
479
                $token = apply_filters(
32✔
480
                        'graphql_woocommerce_cart_session_before_token_sign',
32✔
481
                        $token,
32✔
482
                        $this->_customer_id,
32✔
483
                        $this->_data
32✔
484
                );
32✔
485

486
                // Encode the token.
487
                JWT::$leeway = 60;
32✔
488
                $token       = JWT::encode( $token, $this->get_secret_key(), 'HS256' );
32✔
489

490
                /**
491
                 * Filter the token before returning it, allowing for individual systems to override what's returned.
492
                 *
493
                 * For example, if the user should not be granted a token for whatever reason, a filter could have the token return null.
494
                 *
495
                 * @param string     $token         The signed JWT token that will be returned
496
                 * @param int|string $customer_id   ID of customer associated with token.
497
                 * @param array      $session_data  Session data associated with token.
498
                 */
499
                $token = apply_filters(
32✔
500
                        'graphql_woocommerce_cart_session_signed_token',
32✔
501
                        $token,
32✔
502
                        $this->_customer_id,
32✔
503
                        $this->_data
32✔
504
                );
32✔
505

506
                return $token;
32✔
507
        }
508

509
        /**
510
         * Build a Store API compatible Cart-Token JWT.
511
         *
512
         * Generates a JWT token compatible with WooCommerce Store API (used by WooCommerce Blocks).
513
         * This enables session sharing between GraphQL mutations and WooCommerce Blocks cart/checkout.
514
         *
515
         * @since 0.22.0
516
         *
517
         * @return string|null Cart-Token JWT or null if feature disabled or unavailable.
518
         */
519
        public function build_cart_token() {
520
                // Check if Store API token generation is enabled.
521
                $token_type = woographql_setting( 'set_session_token_type', 'legacy' );
28✔
522
                if ( ! in_array( $token_type, [ 'store-api', 'both' ], true ) ) {
28✔
523
                        return null;
26✔
524
                }
525

526
                // Ensure session is active.
527
                if ( empty( $this->_session_issued ) || ! $this->sending_token() ) {
2✔
528
                        return null;
×
529
                }
530

531
                // Check if WooCommerce Store API utilities are available.
532
                if ( ! $this->supports_store_api() ) {
2✔
533
                        return null;
×
534
                }
535

536
                // Generate Cart-Token using WooCommerce's Store API pattern.
537
                try {
538
                        $token = \Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken::create(
2✔
539
                                [
2✔
540
                                        'user_id' => $this->_customer_id,
2✔
541
                                        'exp'     => $this->_session_expiration,
2✔
542
                                        'iss'     => 'store-api',
2✔
543
                                ],
2✔
544
                                '@' . wp_salt()
2✔
545
                        );
2✔
546

547
                        /**
548
                         * Filter the Store API Cart-Token before returning.
549
                         *
550
                         * @since 0.22.0
551
                         *
552
                         * @param string     $token         The signed Cart-Token JWT
553
                         * @param int|string $customer_id   ID of customer associated with token
554
                         * @param array      $session_data  Session data associated with token
555
                         */
556
                        $token = apply_filters(
2✔
557
                                'graphql_woocommerce_store_api_cart_token',
2✔
558
                                $token,
2✔
559
                                $this->_customer_id,
2✔
560
                                $this->_data
2✔
561
                        );
2✔
562

563
                        return $token;
2✔
564
                } catch ( \Throwable $e ) {
×
565
                        // Log error but don't break GraphQL response.
566
                        do_action( 'graphql_debug', sprintf( 'Failed to generate Cart-Token: %s', $e->getMessage() ) );
×
567
                        return null;
×
568
                }
569
        }
570

571
        /**
572
         * Check if WooCommerce version supports Store API.
573
         *
574
         * Store API Cart-Token functionality requires WooCommerce 5.5.0+.
575
         *
576
         * @since 0.22.0
577
         *
578
         * @return bool
579
         */
580
        protected function supports_store_api() {
581
                // Check WooCommerce is active.
582
                if ( ! defined( 'WC_VERSION' ) ) {
2✔
583
                        return false;
×
584
                }
585

586
                // Store API CartTokenUtils introduced in WC 5.5.0.
587
                if ( version_compare( WC_VERSION, '5.5.0', '<' ) ) {
2✔
588
                        return false;
×
589
                }
590

591
                // Check if Store API JWT class is available.
592
                if ( ! class_exists( '\Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken' ) ) {
2✔
593
                        return false;
×
594
                }
595

596
                return true;
2✔
597
        }
598

599
        /**
600
         * Sets the session header on-demand (usually after adding an item to the cart).
601
         *
602
         * Warning: Headers will only be set if this is called before the headers are sent.
603
         *
604
         * @param bool $set Should the session cookie be set.
605
         *
606
         * @return void
607
         */
608
        public function set_customer_session_token( $set ) {
609
                if ( ! empty( $this->_session_issued ) && $set ) {
33✔
610
                        /**
611
                         * Set callback session token(s) for use in the HTTP response headers.
612
                         * Depending on the session token type setting, this may send:
613
                         * - Legacy GraphQL session token (woocommerce-session header)
614
                         * - Store API Cart-Token header
615
                         * - Both headers
616
                         */
617
                        add_filter(
33✔
618
                                'graphql_response_headers_to_send',
33✔
619
                                function ( $headers ) {
33✔
620
                                        // Add legacy GraphQL session token if enabled.
621
                                        $token = $this->build_token();
28✔
622
                                        if ( $token ) {
28✔
623
                                                $headers[ $this->_token ] = $token;
27✔
624
                                        }
625

626
                                        // Add Store API Cart-Token if enabled.
627
                                        $cart_token = $this->build_cart_token();
28✔
628
                                        if ( ! empty( $cart_token ) ) {
28✔
629
                                                $headers['Cart-Token'] = $cart_token;
2✔
630
                                        }
631

632
                                        return $headers;
28✔
633
                                },
33✔
634
                                10
33✔
635
                        );
33✔
636

637
                        $this->_issuing_new_token = true;
33✔
638
                }
639
        }
640

641
        /**
642
         * {@inheritDoc}
643
         *
644
         * @return void
645
         */
646
        public function set_customer_session_cookie( $set ) {
647
                parent::set_customer_session_cookie( $set );
×
648

649
                if ( $set ) {
×
650
                        $this->_issuing_new_cookie = true;
×
651
                }
652
        }
653

654
        /**
655
         * Return true if the current user has an active session, i.e. a cookie to retrieve values.
656
         *
657
         * @return bool
658
         */
659
        public function has_session() {
660

661
                // @codingStandardsIgnoreLine.
662
                return $this->_issuing_new_token || $this->_has_token || parent::has_session();
225✔
663
        }
664

665
        /**
666
         * Set session expiration.
667
         *
668
         * @return void
669
         */
670
        public function set_session_expiration() {
671
                $this->_session_issued = time();
38✔
672

673
                parent::set_session_expiration();
38✔
674

675
                $this->_session_expiration = apply_filters_deprecated(
38✔
676
                        'graphql_woocommerce_cart_session_expire',
38✔
677
                        [ $this->_session_expiration ],
38✔
678
                        '0.21.0',
38✔
679
                        'wc_session_expiration'
38✔
680
                );
38✔
681
        }
682

683
        /**
684
         * Save any changes to database after a session mutations has been run.
685
         *
686
         * @return void
687
         */
688
        public function save_if_dirty() {
689
                // Update if user recently authenticated.
690
                if ( is_user_logged_in() && get_current_user_id() !== $this->_customer_id ) {
22✔
691
                        $this->_customer_id = get_current_user_id();
7✔
692
                        $this->_dirty       = true;
7✔
693
                }
694

695
                // Bail if no changes.
696
                if ( ! $this->_dirty ) {
22✔
UNCOV
697
                        return;
×
698
                }
699

700
                $this->save_data();
22✔
701
        }
702

703
        /**
704
         * For refreshing session data mid-request when changes occur in concurrent requests.
705
         *
706
         * @return void
707
         */
708
        public function reload_data() {
709
                \WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
22✔
710

711
                // Get session data.
712
                $data = $this->get_session( (string) $this->_customer_id );
22✔
713
                if ( is_array( $data ) ) {
22✔
714
                        $this->_data = $data;
10✔
715
                }
716
        }
717

718
        /**
719
         * Returns "client_session_id". "client_session_id_expiration" is used
720
         * to keep "client_session_id" as fresh as possible.
721
         *
722
         * For the most strict level of security it's highly recommend these values
723
         * be set client-side using the `updateSession` mutation.
724
         * "client_session_id" in particular should be salted with some
725
         * kind of client identifier like the end-user "IP" or "user-agent"
726
         * then hashed parodying the tokens generated by
727
         * WP's WP_Session_Tokens class.
728
         *
729
         * @return string
730
         */
731
        public function get_client_session_id() {
732
                // Get client session ID.
733
                $client_session_id            = $this->get( 'client_session_id', false );
4✔
734
                $client_session_id_expiration = absint( $this->get( 'client_session_id_expiration', 0 ) );
4✔
735

736
                // If client session ID valid return it.
737
                if ( false !== $client_session_id && time() < $client_session_id_expiration ) {
4✔
738
                        // @phpstan-ignore-next-line
739
                        return $client_session_id;
4✔
740
                }
741

742
                // Generate a new client session ID.
743
                $client_session_id            = uniqid();
4✔
744
                $client_session_id_expiration = time() + 3600;
4✔
745
                $this->set( 'client_session_id', $client_session_id );
4✔
746
                $this->set( 'client_session_id_expiration', $client_session_id_expiration );
4✔
747
                $this->save_data();
4✔
748

749
                // Return new client session ID.
750
                return $client_session_id;
4✔
751
        }
752
}
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