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

wp-graphql / wp-graphql-woocommerce / 27452430870

13 Jun 2026 01:26AM UTC coverage: 91.8%. Remained the same
27452430870

Pull #1019

github

web-flow
Merge f03617ca3 into 2ce9424e1
Pull Request #1019: fix: address WordPress.org plugin review (rename + prefixing + headers)

1330 of 1587 new or added lines in 201 files covered. (83.81%)

1 existing line in 1 file now uncovered.

18528 of 20183 relevant lines covered (91.8%)

152.68 hits per line

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

84.62
/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();
42✔
64

65
                $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' );
42✔
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 );
153✔
82
                return null !== $server_key
153✔
83
                        ? 'HTTP_' . strtoupper( $server_key )
153✔
84
                        : '';
153✔
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. Fallback must be at least
96
                // 32 bytes to satisfy php-jwt v7's HS256 minimum key length requirement.
97
                $secret_key = defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) && GRAPHQL_WOOCOMMERCE_SECRET_KEY !== false && GRAPHQL_WOOCOMMERCE_SECRET_KEY !== ''
36✔
98
                        ? GRAPHQL_WOOCOMMERCE_SECRET_KEY :
36✔
99
                        wp_salt();
×
100
                return apply_filters( 'graphql_woocommerce_secret_key', $secret_key );
36✔
101
        }
102

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

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

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

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

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

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

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

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

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

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

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

212
                $start_new_session = ! $token || is_wp_error( $token );
153✔
213
                if ( ! $start_new_session ) {
153✔
214
                        return;
23✔
215
                }
216

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

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

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

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

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

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

266
                if ( empty( $session_header ) ) {
153✔
267
                        return false;
153✔
268
                }
269

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

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

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

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

296
                list( $token ) = $token_string;
25✔
297

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

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

313
                        // Check if token was successful decoded.
314
                        if ( ! $token ) {
25✔
NEW
315
                                throw new \Exception( __( 'Failed to decode session token', 'graphql-for-ecommerce' ) );
×
316
                        }
317

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

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

331
                return $token;
24✔
332
        }
333

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

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

356
                        if ( ! $is_valid ) {
×
NEW
357
                                throw new \Exception( __( 'Invalid Cart-Token', 'graphql-for-ecommerce' ) );
×
358
                        }
359

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

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

375
                return $payload;
×
376
        }
377

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

507
                return $token;
36✔
508
        }
509

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

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

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

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

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

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

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

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

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

597
                return true;
2✔
598
        }
599

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

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

633
                                        return $headers;
32✔
634
                                },
37✔
635
                                10
37✔
636
                        );
37✔
637

638
                        $this->_issuing_new_token = true;
37✔
639
                }
640
        }
641

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

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

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

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

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

674
                parent::set_session_expiration();
42✔
675

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

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

696
                // Bail if no changes.
697
                if ( ! $this->_dirty ) {
25✔
698
                        return;
1✔
699
                }
700

701
                $this->save_data();
25✔
702
        }
703

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

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

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

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

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

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