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

wp-graphql / wp-graphql-woocommerce / 5604143621

pending completion
5604143621

push

github

web-flow
chore: implement WPGraphQL Coding Standards (#769)

* chore: avoid magic constants [PHPCS]

* chore: change ruleset to wp-graphql-cs and lint

* devops: HPOS added back to CI with the release of WC 3.9

---------

Co-authored-by: Geoff Taylor <geoff@axistaylor.com>

562 of 562 new or added lines in 77 files covered. (100.0%)

10309 of 12440 relevant lines covered (82.87%)

53.98 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 WC_Session_Handler;
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 );
31✔
73
                return null !== $server_key
31✔
74
                        ? 'HTTP_' . strtoupper( $server_key )
31✔
75
                        : '';
31✔
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();
25✔
101
                Session_Transaction_Manager::get( $this );
25✔
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 );
25✔
109
                add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 );
25✔
110
                add_action( 'graphql_after_resolve_field', [ $this, 'save_if_dirty' ], 10, 4 );
25✔
111
                add_action( 'shutdown', [ $this, 'save_data' ] );
25✔
112
                add_action( 'wp_logout', [ $this, 'destroy_session' ] );
25✔
113

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

239
                return $token;
2✔
240
        }
241

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

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

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

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

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

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

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

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

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

332
                return $token;
6✔
333
        }
334

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

500
                // Return new client session ID.
501
                return $client_session_id;
2✔
502
        }
503
}
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