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

wp-graphql / wp-graphql-woocommerce / 7340331855

27 Dec 2023 05:01PM UTC coverage: 84.726% (-0.05%) from 84.771%
7340331855

push

github

web-flow
fix: Session transaction queue fix (#832)

* fix: Session transaction queue pop relocated to earlier occurrence in request

* chore: Linter and PHPStan compliance met

2 of 5 new or added lines in 1 file covered. (40.0%)

4 existing lines in 2 files now uncovered.

11083 of 13081 relevant lines covered (84.73%)

58.79 hits per line

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

80.26
/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\WooCommerce\Vendor\Firebase\JWT\JWT;
13
use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\Key;
14

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

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

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

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

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

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

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

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

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

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

113
                if ( ! is_user_logged_in() ) {
27✔
114
                        add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 );
1✔
115
                }
116
        }
117

118
        /**
119
         * Setup token and customer ID.
120
         *
121
         * @throws \GraphQL\Error\UserError Invalid token.
122
         *
123
         * @return void
124
         */
125
        public function init_session_token() {
126
                $token = $this->get_session_token();
33✔
127

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

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

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

148
                                // @phpstan-ignore-next-line
149
                                $this->save_data( $guest_session_id );
×
150
                                $this->set_customer_session_token( true );
×
151
                        }
152

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

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

171
                        // Start new session.
172
                        $this->set_session_expiration();
33✔
173

174
                        // Get Customer ID.
175
                        $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
33✔
176
                        $this->_data        = $this->get_session_data();
33✔
177
                        $this->set_customer_session_token( true );
33✔
178
                }//end if
179
        }
180

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

193
                if ( empty( $session_header ) ) {
33✔
194
                        return false;
33✔
195
                }
196

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

203
                list( $token ) = $token_string;
2✔
204

205
                /**
206
                 * Try to decode the token
207
                 */
208
                try {
209
                        JWT::$leeway = 60;
2✔
210

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

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

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

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

238
                return $token;
2✔
239
        }
240

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

249
                // Looking for the cart session header.
250
                $session_header = isset( $_SERVER[ $session_header_key ] )
33✔
251
                        ? $_SERVER[ $session_header_key ] //@codingStandardsIgnoreLine
3✔
252
                        : false;
33✔
253

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

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

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

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

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

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

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

331
                return $token;
6✔
332
        }
333

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

356
                                        return $headers;
1✔
357
                                },
33✔
358
                                10
33✔
359
                        );
33✔
360

361
                        $this->_issuing_new_token = true;
33✔
362
                }
363
        }
364

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

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

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

405
                // Start new session.
406
                $this->set_session_expiration();
1✔
407

408
                // Get Customer ID.
409
                $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
1✔
410
        }
411

412
        /**
413
         * Save any changes to database after a session mutations has been run.
414
         *
415
         * @return void
416
         */
417
        public function save_if_dirty() {
418
                // Update if user recently authenticated.
UNCOV
419
                if ( is_user_logged_in() && get_current_user_id() !== $this->_customer_id ) {
×
420
                        $this->_customer_id = get_current_user_id();
×
421
                        $this->_dirty       = true;
×
422
                }
423

424
                // Bail if no changes.
UNCOV
425
                if ( ! $this->_dirty ) {
×
UNCOV
426
                        return;
×
427
                }
428

429
                $this->save_data();
×
430
        }
431

432
        /**
433
         * For refreshing session data mid-request when changes occur in concurrent requests.
434
         *
435
         * @return void
436
         */
437
        public function reload_data() {
438
                \WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
×
439

440
                // Get session data.
441
                $data = $this->get_session( (string) $this->_customer_id );
×
442
                if ( is_array( $data ) ) {
×
443
                        $this->_data = $data;
×
444
                }
445
        }
446

447
        /**
448
         * Noop for \WC_Session_Handler method.
449
         *
450
         * Prevents potential crticial errors when calling this method.
451
         *
452
         * @param bool $set Should the session cookie be set.
453
         *
454
         * @return void
455
         */
456
        public function set_customer_session_cookie( $set ) {}
457

458
        /**
459
         * Returns "client_session_id". "client_session_id_expiration" is used
460
         * to keep "client_session_id" as fresh as possible.
461
         *
462
         * For the most strict level of security it's highly recommend these values
463
         * be set client-side using the `updateSession` mutation.
464
         * "client_session_id" in particular should be salted with some
465
         * kind of client identifier like the end-user "IP" or "user-agent"
466
         * then hashed parodying the tokens generated by
467
         * WP's WP_Session_Tokens class.
468
         *
469
         * @return string
470
         */
471
        public function get_client_session_id() {
472
                // Get client session ID.
473
                $client_session_id            = $this->get( 'client_session_id', false );
2✔
474
                $client_session_id_expiration = absint( $this->get( 'client_session_id_expiration', 0 ) );
2✔
475

476
                // If client session ID valid return it.
477
                if ( false !== $client_session_id && time() < $client_session_id_expiration ) {
2✔
478
                        // @phpstan-ignore-next-line
479
                        return $client_session_id;
2✔
480
                }
481

482
                // Generate a new client session ID.
483
                $client_session_id            = uniqid();
2✔
484
                $client_session_id_expiration = time() + 3600;
2✔
485
                $this->set( 'client_session_id', $client_session_id );
2✔
486
                $this->set( 'client_session_id_expiration', $client_session_id_expiration );
2✔
487
                $this->save_data();
2✔
488

489
                // Return new client session ID.
490
                return $client_session_id;
2✔
491
        }
492
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc