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

wp-graphql / wp-graphql-woocommerce / 5603554109

pending completion
5603554109

push

github

web-flow
chore: use fully-qualified class names for PHPDoc types (#767)

* fix!: update deps to required versions

* chore: use fqcn for PHPDoc types

* chore: cleanup use statements

7 of 7 new or added lines in 7 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 WC_Session_Handler;
12
use Firebase\JWT\JWT;
13
use 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
        /**
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
         * Constructor for the session class.
54
         */
55
        public function __construct() {
56
                $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' );
9✔
57
                $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
9✔
58
        }
59

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

240
                return $token;
2✔
241
        }
242

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

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

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

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

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

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

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

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

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

333
                return $token;
6✔
334
        }
335

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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