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

wp-graphql / wp-graphql-woocommerce / 5603512240

pending completion
5603512240

push

github

web-flow
fix: use static closures when possible (#764)

* fix!: update deps to required versions

* fix: use static closures when possible

337 of 337 new or added lines in 79 files covered. (100.0%)

10258 of 12448 relevant lines covered (82.41%)

54.0 hits per line

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

82.58
/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 Firebase\JWT\JWT;
12
use Firebase\JWT\Key;
13
use GraphQL\Error\UserError;
14
use WC_Session_Handler;
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
        /**
26
         * Stores the name of the HTTP header used to pass the session token.
27
         *
28
         * @var string $_token
29
         */
30
        protected $_token; // @codingStandardsIgnoreLine
31

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

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

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

53
        /**
54
         * Constructor for the session class.
55
         */
56
        public function __construct() {
57
                $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' );
9✔
58
                $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
9✔
59
        }
60

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

80
        /**
81
         * This returns the secret key, using the defined constant if defined, and passing it through a filter to
82
         * allow for the config to be able to be set via another method other than a defined constant, such as an
83
         * admin UI that allows the key to be updated/changed/revoked at any time without touching server files
84
         *
85
         * @return mixed|null|string
86
         */
87
        private function get_secret_key() {
88
                // Use the defined secret key, if it exists.
89

90
                $secret_key = defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) && ! empty( GRAPHQL_WOOCOMMERCE_SECRET_KEY )
6✔
91
                        ? GRAPHQL_WOOCOMMERCE_SECRET_KEY :
6✔
92
                        'graphql-woo-cart-session';
6✔
93
                return apply_filters( 'graphql_woocommerce_secret_key', $secret_key );
6✔
94
        }
95

96
        /**
97
         * Init hooks and session data.
98
         *
99
         * @return void
100
         */
101
        public function init() {
102
                $this->init_session_token();
25✔
103
                Session_Transaction_Manager::get( $this );
25✔
104

105
                /**
106
                 *  Necessary since Session_Transaction_Manager applies to the reference.
107
                 *
108
                 * @var self $this
109
                 */
110
                add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 );
25✔
111
                add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 );
25✔
112
                add_action( 'graphql_after_resolve_field', [ $this, 'save_if_dirty' ], 10, 4 );
25✔
113
                add_action( 'shutdown', [ $this, 'save_data' ] );
25✔
114
                add_action( 'wp_logout', [ $this, 'destroy_session' ] );
25✔
115

116
                if ( ! is_user_logged_in() ) {
25✔
117
                        add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 );
1✔
118
                }
119
        }
120

121
        /**
122
         * Setup token and customer ID.
123
         *
124
         * @throws UserError Invalid token.
125
         *
126
         * @return void
127
         */
128
        public function init_session_token() {
129
                $token = $this->get_session_token();
31✔
130

131
                // Process existing session if not expired or invalid.
132
                if ( $token && is_object( $token ) && ! is_wp_error( $token ) ) {
31✔
133
                        $this->_customer_id        = $token->data->customer_id;
1✔
134
                        $this->_session_issued     = $token->iat;
1✔
135
                        $this->_session_expiration = $token->exp;
1✔
136
                        $this->_session_expiring   = $token->exp - ( 3600 );
1✔
137
                        $this->_has_token          = true;
1✔
138
                        $this->_data               = $this->get_session_data();
1✔
139

140
                        // If the user logs in, update session.
141
                        if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) {
1✔
142
                                $guest_session_id   = $this->_customer_id;
×
143
                                $this->_customer_id = strval( get_current_user_id() );
×
144
                                $this->_dirty       = true;
×
145

146
                                // If session empty check for previous data associated with customer and assign that to the session.
147
                                if ( empty( $this->_data ) ) {
×
148
                                        $this->_data = $this->get_session_data();
×
149
                                }
150

151
                                // @phpstan-ignore-next-line
152
                                $this->save_data( $guest_session_id );
×
153
                                $this->set_customer_session_token( true );
×
154
                        }
155

156
                        // Update session expiration on each action.
157
                        $this->set_session_expiration();
1✔
158
                        if ( $token->exp < $this->_session_expiration ) {
1✔
159
                                $this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration );
1✔
160
                        }
161
                } else {
162

163
                        // If token invalid throw warning.
164
                        if ( is_wp_error( $token ) ) {
31✔
165
                                add_filter(
×
166
                                        'graphql_woocommerce_session_token_errors',
×
167
                                        static function( $errors ) use ( $token ) {
×
168
                                                $errors = $token->get_error_message();
×
169
                                                return $errors;
×
170
                                        }
×
171
                                );
×
172
                        }
173

174
                        // Start new session.
175
                        $this->set_session_expiration();
31✔
176

177
                        // Get Customer ID.
178
                        $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
31✔
179
                        $this->_data        = $this->get_session_data();
31✔
180
                        $this->set_customer_session_token( true );
31✔
181
                }//end if
182
        }
183

184
        /**
185
         * Retrieve and decrypt the session data from session, if set. Otherwise return false.
186
         *
187
         * Session cookies without a customer ID are invalid.
188
         *
189
         * @throws \Exception  Invalid token.
190
         * @return false|\WP_Error|object{ iat: int, exp: int, data: object{ customer_id: string } }
191
         */
192
        public function get_session_token() {
193
                // Get the Auth header.
194
                $session_header = $this->get_session_header();
31✔
195

196
                if ( empty( $session_header ) ) {
31✔
197
                        return false;
31✔
198
                }
199

200
                // Get the token from the header.
201
                $token_string = sscanf( $session_header, 'Session %s' );
2✔
202
                if ( empty( $token_string ) ) {
2✔
203
                        return false;
×
204
                }
205

206
                list( $token ) = $token_string;
2✔
207

208
                /**
209
                 * Try to decode the token
210
                 */
211
                try {
212
                        JWT::$leeway = 60;
2✔
213

214
                        $secret = $this->get_secret_key();
2✔
215
                        $key    = new Key( $secret, 'HS256' );
2✔
216
                        /**
217
                         * Decode the token
218
                         *
219
                         * @var null|object{ iat: int, exp: int, data: object{ customer_id: string }, iss: string } $token
220
                         */
221
                        $token = ! empty( $token ) ? JWT::decode( $token, $key ) : null;
2✔
222

223
                        // Check if token was successful decoded.
224
                        if ( ! $token ) {
2✔
225
                                throw new \Exception( __( 'Failed to decode session token', 'wp-graphql-woocommerce' ) );
×
226
                        }
227

228
                        // The Token is decoded now validate the iss.
229
                        if ( empty( $token->iss ) || get_bloginfo( 'url' ) !== $token->iss ) {
2✔
230
                                throw new \Exception( __( 'The iss do not match with this server', 'wp-graphql-woocommerce' ) );
×
231
                        }
232

233
                        // Validate the customer id in the token.
234
                        if ( empty( $token->data ) || empty( $token->data->customer_id ) ) {
2✔
235
                                throw new \Exception( __( 'Customer ID not found in the token', 'wp-graphql-woocommerce' ) );
2✔
236
                        }
237
                } catch ( \Exception $error ) {
×
238
                        return new \WP_Error( 'invalid_token', $error->getMessage() );
×
239
                }//end try
240

241
                return $token;
2✔
242
        }
243

244
        /**
245
         * Get the value of the cart session header from the $_SERVER super global
246
         *
247
         * @return mixed|string
248
         */
249
        public function get_session_header() {
250
                $session_header_key = $this->get_server_key();
31✔
251

252
                // Looking for the cart session header.
253
                $session_header = isset( $_SERVER[ $session_header_key ] )
31✔
254
                        ? $_SERVER[ $session_header_key ] //@codingStandardsIgnoreLine
3✔
255
                        : false;
31✔
256

257
                /**
258
                 * Return the cart session header, passed through a filter
259
                 *
260
                 * @param string $session_header  The header used to identify a user's cart session token.
261
                 */
262
                return apply_filters( 'graphql_woocommerce_cart_session_header', $session_header );
31✔
263
        }
264

265
        /**
266
         * Creates JSON Web Token for customer session.
267
         *
268
         * @return false|string
269
         */
270
        public function build_token() {
271
                if ( empty( $this->_session_issued ) ) {
6✔
272
                        return false;
1✔
273
                }
274

275
                /**
276
                 * Determine the "not before" value for use in the token
277
                 *
278
                 * @param float      $issued        The timestamp of token was issued.
279
                 * @param int|string $customer_id   Customer ID.
280
                 * @param array      $session_data  Cart session data.
281
                 */
282
                $not_before = apply_filters(
6✔
283
                        'graphql_woo_cart_session_not_before',
6✔
284
                        $this->_session_issued,
6✔
285
                        $this->_customer_id,
6✔
286
                        $this->_data
6✔
287
                );
6✔
288

289
                // Configure the token array, which will be encoded.
290
                $token = [
6✔
291
                        'iss'  => get_bloginfo( 'url' ),
6✔
292
                        'iat'  => $this->_session_issued,
6✔
293
                        'nbf'  => $not_before,
6✔
294
                        'exp'  => $this->_session_expiration,
6✔
295
                        'data' => [
6✔
296
                                'customer_id' => $this->_customer_id,
6✔
297
                        ],
6✔
298
                ];
6✔
299

300
                /**
301
                 * Filter the token, allowing for individual systems to configure the token as needed
302
                 *
303
                 * @param array      $token         The token array that will be encoded
304
                 * @param int|string $customer_id   ID of customer associated with token.
305
                 * @param array      $session_data  Session data associated with token.
306
                 */
307
                $token = apply_filters(
6✔
308
                        'graphql_woocommerce_cart_session_before_token_sign',
6✔
309
                        $token,
6✔
310
                        $this->_customer_id,
6✔
311
                        $this->_data
6✔
312
                );
6✔
313

314
                // Encode the token.
315
                JWT::$leeway = 60;
6✔
316
                $token       = JWT::encode( $token, $this->get_secret_key(), 'HS256' );
6✔
317

318
                /**
319
                 * Filter the token before returning it, allowing for individual systems to override what's returned.
320
                 *
321
                 * For example, if the user should not be granted a token for whatever reason, a filter could have the token return null.
322
                 *
323
                 * @param string     $token         The signed JWT token that will be returned
324
                 * @param int|string $customer_id   ID of customer associated with token.
325
                 * @param array      $session_data  Session data associated with token.
326
                 */
327
                $token = apply_filters(
6✔
328
                        'graphql_woocommerce_cart_session_signed_token',
6✔
329
                        $token,
6✔
330
                        $this->_customer_id,
6✔
331
                        $this->_data
6✔
332
                );
6✔
333

334
                return $token;
6✔
335
        }
336

337
        /**
338
         * Sets the session header on-demand (usually after adding an item to the cart).
339
         *
340
         * Warning: Headers will only be set if this is called before the headers are sent.
341
         *
342
         * @param bool $set Should the session cookie be set.
343
         *
344
         * @return void
345
         */
346
        public function set_customer_session_token( $set ) {
347
                if ( ! empty( $this->_session_issued ) && $set ) {
31✔
348
                        /**
349
                         * Set callback session token for use in the HTTP response header and customer/user "sessionToken" field.
350
                         */
351
                        add_filter(
31✔
352
                                'graphql_response_headers_to_send',
31✔
353
                                function( $headers ) {
31✔
354
                                        $token = $this->build_token();
1✔
355
                                        if ( $token ) {
1✔
356
                                                $headers[ $this->_token ] = $token;
1✔
357
                                        }
358

359
                                        return $headers;
1✔
360
                                },
31✔
361
                                10
31✔
362
                        );
31✔
363

364
                        $this->_issuing_new_token = true;
31✔
365
                }
366
        }
367

368
        /**
369
         * Return true if the current user has an active session, i.e. a cookie to retrieve values.
370
         *
371
         * @return bool
372
         */
373
        public function has_session() {
374
                // @codingStandardsIgnoreLine.
375
                return $this->_issuing_new_token || $this->_has_token || is_user_logged_in();
32✔
376
        }
377

378
        /**
379
         * Set session expiration.
380
         *
381
         * @return void
382
         */
383
        public function set_session_expiration() {
384
                $this->_session_issued = time();
32✔
385
                // 14 Days.
386
                $this->_session_expiration = apply_filters(
32✔
387
                        'graphql_woocommerce_cart_session_expire',
32✔
388
                        // Seconds * Minutes * Hours * Days.
389
                        $this->_session_issued + ( 60 * 60 * 24 * 14 )
32✔
390
                );
32✔
391
                // 13 Days.
392
                $this->_session_expiring = $this->_session_expiration - ( 60 * 60 * 24 );
32✔
393
        }
394

395
        /**
396
         * Forget all session data without destroying it.
397
         *
398
         * @return void
399
         */
400
        public function forget_session() {
401
                if ( isset( $this->_token_to_be_sent ) ) {
1✔
402
                        unset( $this->_token_to_be_sent );
×
403
                }
404
                wc_empty_cart();
1✔
405
                $this->_data  = [];
1✔
406
                $this->_dirty = false;
1✔
407

408
                // Start new session.
409
                $this->set_session_expiration();
1✔
410

411
                // Get Customer ID.
412
                $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
1✔
413
        }
414

415
        /**
416
         * Save any changes to database after a session mutations has been run.
417
         *
418
         * @param mixed                                $source   Operation root object.
419
         * @param array                                $args     Operation arguments.
420
         * @param \WPGraphQL\AppContext                $context  AppContext instance.
421
         * @param \GraphQL\Type\Definition\ResolveInfo $info     Operation ResolveInfo object.
422
         *
423
         * @return void
424
         */
425
        public function save_if_dirty( $source, $args, $context, $info ) {
426
                // Bail early, if not one of the session mutations.
427
                if ( ! in_array( $info->fieldName, Session_Transaction_Manager::get_session_mutations(), true ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
25✔
428
                        return;
25✔
429
                }
430

431
                // Update if user recently authenticated.
432
                if ( is_user_logged_in() && get_current_user_id() !== $this->_customer_id ) {
1✔
433
                        $this->_customer_id = get_current_user_id();
×
434
                        $this->_dirty       = true;
×
435
                }
436

437
                // Bail if no changes.
438
                if ( ! $this->_dirty ) {
1✔
439
                        return;
1✔
440
                }
441

442
                $this->save_data();
×
443
        }
444

445
        /**
446
         * For refreshing session data mid-request when changes occur in concurrent requests.
447
         *
448
         * @return void
449
         */
450
        public function reload_data() {
451
                \WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
×
452

453
                // Get session data.
454
                $data = $this->get_session( (string) $this->_customer_id );
×
455
                if ( is_array( $data ) ) {
×
456
                        $this->_data = $data;
×
457
                }
458
        }
459

460
        /**
461
         * Noop for \WC_Session_Handler method.
462
         *
463
         * Prevents potential crticial errors when calling this method.
464
         *
465
         * @param bool $set Should the session cookie be set.
466
         *
467
         * @return void
468
         */
469
        public function set_customer_session_cookie( $set ) {}
470

471
        /**
472
         * Returns "client_session_id". "client_session_id_expiration" is used
473
         * to keep "client_session_id" as fresh as possible.
474
         *
475
         * For the most strict level of security it's highly recommend these values
476
         * be set client-side using the `updateSession` mutation.
477
         * "client_session_id" in particular should be salted with some
478
         * kind of client identifier like the end-user "IP" or "user-agent"
479
         * then hashed parodying the tokens generated by
480
         * WP's WP_Session_Tokens class.
481
         *
482
         * @return string
483
         */
484
        public function get_client_session_id() {
485
                // Get client session ID.
486
                $client_session_id            = $this->get( 'client_session_id', false );
2✔
487
                $client_session_id_expiration = absint( $this->get( 'client_session_id_expiration', 0 ) );
2✔
488

489
                // If client session ID valid return it.
490
                if ( false !== $client_session_id && time() < $client_session_id_expiration ) {
2✔
491
                        // @phpstan-ignore-next-line
492
                        return $client_session_id;
2✔
493
                }
494

495
                // Generate a new client session ID.
496
                $client_session_id            = uniqid();
2✔
497
                $client_session_id_expiration = time() + 3600;
2✔
498
                $this->set( 'client_session_id', $client_session_id );
2✔
499
                $this->set( 'client_session_id_expiration', $client_session_id_expiration );
2✔
500
                $this->save_data();
2✔
501

502
                // Return new client session ID.
503
                return $client_session_id;
2✔
504
        }
505

506
}
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