From e5c120798d844a4994291379aa7a76233f3423fe Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 19 Jun 2024 17:43:34 -0400 Subject: [PATCH 01/10] feat: QL Session Handler functionality expanded to support cookies on non-GraphQL requests --- codeception.dist.yml | 4 + includes/admin/class-general.php | 18 +++ includes/class-woocommerce-filters.php | 17 ++- includes/utils/class-ql-session-handler.php | 121 +++++++++----------- tests/wpunit/QLSessionHandlerTest.php | 98 +++++++++++++++- 5 files changed, 186 insertions(+), 72 deletions(-) diff --git a/codeception.dist.yml b/codeception.dist.yml index 0935e056..14191bf3 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -66,6 +66,10 @@ modules: uploads: '/wp-content/uploads' WPLoader: wpRootFolder: '%WP_CORE_DIR%' + dbHost: '%DB_HOST%' + dbName: '%DB_NAME%' + dbUser: '%DB_USER%' + dbPassword: '%DB_PASSWORD%' dbUrl: 'mysql://%DB_USER%:%DB_PASSWORD%@%DB_HOST%:%DB_PORT%/%DB_NAME%' dbName: '%DB_NAME%' dbHost: '%DB_HOST%' diff --git a/includes/admin/class-general.php b/includes/admin/class-general.php index ded29c5b..eb27e307 100644 --- a/includes/admin/class-general.php +++ b/includes/admin/class-general.php @@ -77,6 +77,24 @@ public static function get_fields() { 'value' => defined( 'NO_QL_SESSION_HANDLER' ) ? 'on' : woographql_setting( 'disable_ql_session_handler', 'off' ), 'disabled' => defined( 'NO_QL_SESSION_HANDLER' ), ], + [ + 'name' => 'enable_ql_session_handler_on_ajax', + 'label' => __( 'Enable QL Session Handler on WC AJAX requests.', 'wp-graphql-woocommerce' ), + 'desc' => __( 'Enabling this will enable JSON Web Tokens usage on WC AJAX requests.', 'wp-graphql-woocommerce' ) + . ( defined( 'NO_QL_SESSION_HANDLER' ) ? __( ' This setting is disabled. The "NO_QL_SESSION_HANDLER" flag has been triggered with code', 'wp-graphql-woocommerce' ) : '' ), + 'type' => 'checkbox', + 'value' => defined( 'NO_QL_SESSION_HANDLER' ) ? 'off' : woographql_setting( 'enable_ql_session_handler_on_ajax', 'off' ), + 'disabled' => defined( 'NO_QL_SESSION_HANDLER' ), + ], + [ + 'name' => 'enable_ql_session_handler_on_rest', + 'label' => __( 'Enable QL Session Handler on WP REST requests.', 'wp-graphql-woocommerce' ), + 'desc' => __( 'Enabling this will enable JSON Web Tokens usage on WP REST requests.', 'wp-graphql-woocommerce' ) + . ( defined( 'NO_QL_SESSION_HANDLER' ) ? __( ' This setting is disabled. The "NO_QL_SESSION_HANDLER" flag has been triggered with code', 'wp-graphql-woocommerce' ) : '' ), + 'type' => 'checkbox', + 'value' => defined( 'NO_QL_SESSION_HANDLER' ) ? 'off' : woographql_setting( 'enable_ql_session_handler_on_rest', 'off' ), + 'disabled' => defined( 'NO_QL_SESSION_HANDLER' ), + ], [ 'name' => 'enable_unsupported_product_type', 'label' => __( 'Enable Unsupported types', 'wp-graphql-woocommerce' ), diff --git a/includes/class-woocommerce-filters.php b/includes/class-woocommerce-filters.php index a8b85321..f7beef72 100644 --- a/includes/class-woocommerce-filters.php +++ b/includes/class-woocommerce-filters.php @@ -82,6 +82,21 @@ public static function get_authorizing_url_nonce_param_name( $field ) { return woographql_setting( "{$field}_nonce_param", null ); } + public static function should_load_session_handler() { + switch( true ) { + case \WPGraphQL\Router::is_graphql_http_request(): + //phpcs:disable + case 'on' === woographql_setting( 'enable_ql_session_handler_on_ajax', 'off' ) + && ( ! empty( $_GET['wc-ajax'] ) || defined( 'WC_DOING_AJAX' ) ): + //phpcs:enable + case 'on' === woographql_setting( 'enable_ql_session_handler_on_rest', 'off' ) + && ( defined( 'REST_REQUEST' ) && REST_REQUEST ): + return true; + default: + return false; + } + } + /** * WooCommerce Session Handler callback * @@ -89,7 +104,7 @@ public static function get_authorizing_url_nonce_param_name( $field ) { * @return string */ public static function woocommerce_session_handler( $session_class ) { - if ( \WPGraphQL\Router::is_graphql_http_request() ) { + if ( self::should_load_session_handler() ) { $session_class = '\WPGraphQL\WooCommerce\Utils\QL_Session_Handler'; } elseif ( WooGraphQL::auth_router_is_enabled() ) { require_once get_includes_directory() . 'utils/class-protected-router.php'; diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index 761e214c..79157d07 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -11,6 +11,7 @@ use WC_Session_Handler; use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\JWT; use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\Key; +use WPGraphQL\Router as Router; /** * Class - QL_Session_Handler @@ -52,8 +53,8 @@ class QL_Session_Handler extends WC_Session_Handler { * Constructor for the session class. */ public function __construct() { + parent::__construct(); $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' ); - $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions'; } /** @@ -100,18 +101,17 @@ public function init() { $this->init_session_token(); Session_Transaction_Manager::get( $this ); - /** - * Necessary since Session_Transaction_Manager applies to the reference. - * - * @var self $this - */ - add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 ); - add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 ); - add_action( 'shutdown', [ $this, 'save_data' ] ); - add_action( 'wp_logout', [ $this, 'destroy_session' ] ); - - if ( ! is_user_logged_in() ) { - add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 ); + if ( Router::is_graphql_http_request() ) { + add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 ); + add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 ); + add_action( 'shutdown', [ $this, 'save_data' ] ); + } else { + add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_cookie' ], 10 ); + add_action( 'shutdown', [ $this, 'save_data' ], 20 ); + add_action( 'wp_logout', [ $this, 'destroy_session' ] ); + if ( ! is_user_logged_in() ) { + add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 ); + } } } @@ -123,6 +123,10 @@ public function init() { * @return void */ public function init_session_token() { + + /** + * @var object{ iat: int, exp: int, data: object{ customer_id: string } }|false|\WP_Error $token + */ $token = $this->get_session_token(); // Process existing session if not expired or invalid. @@ -147,7 +151,9 @@ public function init_session_token() { // @phpstan-ignore-next-line $this->save_data( $guest_session_id ); - $this->set_customer_session_token( true ); + Router::is_graphql_http_request() + ? $this->set_customer_session_token( true ) + : $this->set_customer_session_cookie( true ); } // Update session expiration on each action. @@ -155,19 +161,22 @@ public function init_session_token() { if ( $token->exp < $this->_session_expiration ) { $this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration ); } - } else { + } else if ( is_wp_error( $token ) ) { + add_filter( + 'graphql_woocommerce_session_token_errors', + static function ( $errors ) use ( $token ) { + $errors = $token->get_error_message(); + return $errors; + } + ); + } - // If token invalid throw warning. - if ( is_wp_error( $token ) ) { - add_filter( - 'graphql_woocommerce_session_token_errors', - static function ( $errors ) use ( $token ) { - $errors = $token->get_error_message(); - return $errors; - } - ); - } + $start_new_session = ! $token || is_wp_error( $token ); + if ( ! $start_new_session ) { + return; + } + if ( Router::is_graphql_http_request() ) { // Start new session. $this->set_session_expiration(); @@ -175,7 +184,10 @@ static function ( $errors ) use ( $token ) { $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id(); $this->_data = $this->get_session_data(); $this->set_customer_session_token( true ); - }//end if + + } else { + return $this->init_session_cookie(); + } } /** @@ -259,13 +271,22 @@ public function get_session_header() { return apply_filters( 'graphql_woocommerce_cart_session_header', $session_header ); } + /** + * Determine if a JWT is being sent in the page response. + * + * @return bool + */ + private function sending_token() { + return $this->_has_token || $this->_issuing_new_token; + } + /** * Creates JSON Web Token for customer session. * * @return false|string */ public function build_token() { - if ( empty( $this->_session_issued ) ) { + if ( empty( $this->_session_issued ) || ! $this->sending_token() ) { return false; } @@ -368,8 +389,9 @@ function ( $headers ) { * @return bool */ public function has_session() { + // @codingStandardsIgnoreLine. - return $this->_issuing_new_token || $this->_has_token || is_user_logged_in(); + return $this->_issuing_new_token || $this->_has_token || parent::has_session(); } /** @@ -378,35 +400,9 @@ public function has_session() { * @return void */ public function set_session_expiration() { - $this->_session_issued = time(); - // 14 Days. - $this->_session_expiration = apply_filters( - 'graphql_woocommerce_cart_session_expire', - // Seconds * Minutes * Hours * Days. - $this->_session_issued + ( 60 * 60 * 24 * 14 ) - ); - // 13 Days. - $this->_session_expiring = $this->_session_expiration - ( 60 * 60 * 24 ); - } - - /** - * Forget all session data without destroying it. - * - * @return void - */ - public function forget_session() { - if ( isset( $this->_token_to_be_sent ) ) { - unset( $this->_token_to_be_sent ); - } - wc_empty_cart(); - $this->_data = []; - $this->_dirty = false; - - // Start new session. - $this->set_session_expiration(); - - // Get Customer ID. - $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id(); + $this->_session_issued = time(); + $this->_session_expiring = apply_filters( 'wc_session_expiring', $this->_session_issued + ( 60 * 60 * 47 ) ); // 47 Hours. + $this->_session_expiration = apply_filters( 'wc_session_expiration', $this->_session_issued + ( 60 * 60 * 48 ) ); // 48 Hours. } /** @@ -444,17 +440,6 @@ public function reload_data() { } } - /** - * Noop for \WC_Session_Handler method. - * - * Prevents potential crticial errors when calling this method. - * - * @param bool $set Should the session cookie be set. - * - * @return void - */ - public function set_customer_session_cookie( $set ) {} - /** * Returns "client_session_id". "client_session_id_expiration" is used * to keep "client_session_id" as fresh as possible. diff --git a/tests/wpunit/QLSessionHandlerTest.php b/tests/wpunit/QLSessionHandlerTest.php index b9afbe51..44e1937b 100644 --- a/tests/wpunit/QLSessionHandlerTest.php +++ b/tests/wpunit/QLSessionHandlerTest.php @@ -11,9 +11,14 @@ define( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY', 'graphql-woo-cart-session' ); } +/** + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + */ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { public function tearDown(): void { unset( $_SERVER ); + WC()->session->destroy_session(); // after parent::tearDown(); @@ -27,7 +32,10 @@ public function test_initializes() { $this->assertInstanceOf( QL_Session_Handler::class, $session ); } - public function test_init_session_token() { + public function test_init_on_graphql_request() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -35,7 +43,7 @@ public function test_init_session_token() { $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); // Initialize session. - $session->init_session_token(); + $session->init(); // Assert session has started. $this->assertTrue( $session->has_session(), 'Should have session.' ); @@ -51,16 +59,91 @@ public function test_init_session_token() { usleep( 1000000 ); // Initialize session token for next request. - $session->init_session_token(); + remove_action( 'woocommerce_set_cart_cookies', [ $session, 'set_customer_session_token' ] ); + remove_action( 'woographql_update_session', [ $session, 'set_customer_session_token' ] ); + remove_action( 'shutdown', [ $session, 'save_data' ] ); + + // Create new session handler. + $session = new QL_Session_Handler(); + $session->init(); $new_token = $session->build_token(); $decoded_new_token = JWT::decode( $new_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) ); // Assert new token is different than old token. $this->assertNotEquals( $old_token, $new_token, 'New token should not match token from last request.' ); $this->assertGreaterThan( $decoded_old_token->exp, $decoded_new_token->exp ); + + // Assert customer ID match + $this->assertEquals( $decoded_old_token->data->customer_id, $decoded_new_token->data->customer_id ); + } + + public function test_init_on_non_graphql_request() { + $product_id = $this->factory()->product->createSimple(); + + // Create session handler. + $session = new QL_Session_Handler(); + + // Assert session hasn't started. + $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); + + // Initialize session. + $session->init(); + + // Add product to cart and start the session. + $this->factory()->cart->add( $product_id ); + + // Assert session has started. + $this->assertTrue( $session->has_session(), 'Should have session.' ); + + // Assert no tokens are being issued. + $this->assertFalse( $session->build_token(), 'Should not have a token.' ); + } + + public function test_init_on_non_graphql_request_with_session_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + + // Create session handler. + $session = new QL_Session_Handler(); + + // Assert session hasn't started. + $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); + + // Initialize session. + $session->init(); + + // Assert session has started. + $this->assertTrue( $session->has_session(), 'Should have session.' ); + + // Get token for future request. + $token_to_session = $session->build_token(); + + // Remove GraphQL HTTP Request filter to simulate normal request. + remove_filter( 'graphql_is_graphql_http_request', '__return_true' ); + + // Sent token to HTTP header to simulate a new request. + $_SERVER['HTTP_WOOCOMMERCE_SESSION'] = 'Session ' . $token_to_session; + + // Create session handler. + $session = new QL_Session_Handler(); + + // Assert session hasn't started. + $this->assertFalse( $session->has_session(), 'Shouldn\'t have a session yet' ); + + // Initialize session. + $session->init(); + + // Assert session has started. + $this->assertTrue( $session->has_session(), 'Should have session.' ); + + // Assert tokens are being issued. + $this->assertNotFalse( $session->build_token() ); } public function test_get_session_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -96,6 +179,9 @@ public function test_get_session_header() { } public function test_build_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -116,6 +202,9 @@ public function test_build_token() { } public function test_set_customer_session_token() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); @@ -131,6 +220,9 @@ public function test_set_customer_session_token() { } public function test_forget_session() { + // Simulate GraphQL HTTP Request. + add_filter( 'graphql_is_graphql_http_request', '__return_true' ); + // Create session handler. $session = new QL_Session_Handler(); $session->init_session_token(); From 59970ef1e4c55caa3edda36e3dc9f34d541068ab Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 19 Jun 2024 18:10:44 -0400 Subject: [PATCH 02/10] chore: Linter and PHPStan compliance met --- includes/class-woocommerce-filters.php | 9 +++++-- includes/utils/class-ql-session-handler.php | 29 +++++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/includes/class-woocommerce-filters.php b/includes/class-woocommerce-filters.php index f7beef72..2a26c878 100644 --- a/includes/class-woocommerce-filters.php +++ b/includes/class-woocommerce-filters.php @@ -82,8 +82,13 @@ public static function get_authorizing_url_nonce_param_name( $field ) { return woographql_setting( "{$field}_nonce_param", null ); } + /** + * Returns true if the session handler should be loaded. + * + * @return boolean + */ public static function should_load_session_handler() { - switch( true ) { + switch ( true ) { case \WPGraphQL\Router::is_graphql_http_request(): //phpcs:disable case 'on' === woographql_setting( 'enable_ql_session_handler_on_ajax', 'off' ) @@ -92,7 +97,7 @@ public static function should_load_session_handler() { case 'on' === woographql_setting( 'enable_ql_session_handler_on_rest', 'off' ) && ( defined( 'REST_REQUEST' ) && REST_REQUEST ): return true; - default: + default: return false; } } diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index 79157d07..ad9ac972 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -9,9 +9,9 @@ namespace WPGraphQL\WooCommerce\Utils; use WC_Session_Handler; +use WPGraphQL\Router; use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\JWT; use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\Key; -use WPGraphQL\Router as Router; /** * Class - QL_Session_Handler @@ -54,6 +54,7 @@ class QL_Session_Handler extends WC_Session_Handler { */ public function __construct() { parent::__construct(); + $this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' ); } @@ -101,6 +102,11 @@ public function init() { $this->init_session_token(); Session_Transaction_Manager::get( $this ); + /** + * Necessary since Session_Transaction_Manager applies to the reference. + * + * @var self $this + */ if ( Router::is_graphql_http_request() ) { add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 ); add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 ); @@ -161,7 +167,7 @@ public function init_session_token() { if ( $token->exp < $this->_session_expiration ) { $this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration ); } - } else if ( is_wp_error( $token ) ) { + } elseif ( is_wp_error( $token ) ) { add_filter( 'graphql_woocommerce_session_token_errors', static function ( $errors ) use ( $token ) { @@ -184,9 +190,8 @@ static function ( $errors ) use ( $token ) { $this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id(); $this->_data = $this->get_session_data(); $this->set_customer_session_token( true ); - } else { - return $this->init_session_cookie(); + $this->init_session_cookie(); } } @@ -389,7 +394,7 @@ function ( $headers ) { * @return bool */ public function has_session() { - + // @codingStandardsIgnoreLine. return $this->_issuing_new_token || $this->_has_token || parent::has_session(); } @@ -400,9 +405,17 @@ public function has_session() { * @return void */ public function set_session_expiration() { - $this->_session_issued = time(); - $this->_session_expiring = apply_filters( 'wc_session_expiring', $this->_session_issued + ( 60 * 60 * 47 ) ); // 47 Hours. - $this->_session_expiration = apply_filters( 'wc_session_expiration', $this->_session_issued + ( 60 * 60 * 48 ) ); // 48 Hours. + $this->_session_issued = time(); + // 47 hours. + $this->_session_expiring = apply_filters( 'wc_session_expiring', $this->_session_issued + ( 60 * 60 * 47 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + // 48 hours. + $this->_session_expiration = apply_filters( 'wc_session_expiration', $this->_session_issued + ( 60 * 60 * 48 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $this->_session_expiration = apply_filters_deprecated( + 'graphql_woocommerce_cart_session_expire', + [ $this->_session_expiration ], + 'TBD', + 'wc_session_expiration' + ); } /** From 3d63cffba379dc64400335ad4ffc5a1e9ff62bf4 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 19 Jun 2024 20:17:37 -0400 Subject: [PATCH 03/10] devops: QLSessionHandlerTest patched for suite testing --- includes/utils/class-ql-session-handler.php | 28 +++++++++++++++++- tests/wpunit/QLSessionHandlerTest.php | 32 +++++++++++++++------ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index ad9ac972..786da4d5 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -49,6 +49,13 @@ class QL_Session_Handler extends WC_Session_Handler { */ protected $_issuing_new_token = false; // @codingStandardsIgnoreLine + /** + * True when a new session cookie has been issued. + * + * @var bool $_issuing_new_cookie + */ + protected $_issuing_new_cookie = false; // @codingStandardsIgnoreLine + /** * Constructor for the session class. */ @@ -281,10 +288,19 @@ public function get_session_header() { * * @return bool */ - private function sending_token() { + public function sending_token() { return $this->_has_token || $this->_issuing_new_token; } + /** + * Determine if a HTTP cookie is being sent in the page response. + * + * @return bool + */ + public function sending_cookie() { + return $this->_has_cookie || $this->_issuing_new_cookie; + } + /** * Creates JSON Web Token for customer session. * @@ -388,6 +404,16 @@ function ( $headers ) { } } + /** + * {@inheritDoc} + */ + public function set_customer_session_cookie( $set ) { + parent::set_customer_session_cookie( $set ); + if ( $set ) { + $this->_issuing_new_cookie = true; + } + } + /** * Return true if the current user has an active session, i.e. a cookie to retrieve values. * diff --git a/tests/wpunit/QLSessionHandlerTest.php b/tests/wpunit/QLSessionHandlerTest.php index 44e1937b..93bb0eb8 100644 --- a/tests/wpunit/QLSessionHandlerTest.php +++ b/tests/wpunit/QLSessionHandlerTest.php @@ -11,11 +11,17 @@ define( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY', 'graphql-woo-cart-session' ); } -/** - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled - */ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { + + public function setUp(): void { + parent::setUp(); + + // before + unset( $_SERVER['HTTP_WOOCOMMERCE_SESSION'] ); + $customer_cookie_key = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); + wc_setcookie( $customer_cookie_key, 0, time() - HOUR_IN_SECONDS ); + unset( $_COOKIE[ $customer_cookie_key ] ); + } public function tearDown(): void { unset( $_SERVER ); WC()->session->destroy_session(); @@ -78,8 +84,6 @@ public function test_init_on_graphql_request() { } public function test_init_on_non_graphql_request() { - $product_id = $this->factory()->product->createSimple(); - // Create session handler. $session = new QL_Session_Handler(); @@ -90,13 +94,23 @@ public function test_init_on_non_graphql_request() { $session->init(); // Add product to cart and start the session. - $this->factory()->cart->add( $product_id ); + $this->factory->cart->add( + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + $this->factory->product->createSimple(), + ); + // Assert session has started. - $this->assertTrue( $session->has_session(), 'Should have session.' ); + //$this->assertTrue( $session->sending_cookie(), 'Issuing new customer session cookie.' ); // Assert no tokens are being issued. - $this->assertFalse( $session->build_token(), 'Should not have a token.' ); + $this->assertFalse( $session->sending_token(), 'Should not be issuing a new customer token.' ); + $this->assertFalse( $session->build_token(), 'Should not be issuing a new customer token.' ); } public function test_init_on_non_graphql_request_with_session_token() { From 1e9ff3816713472b5534b200100426e5a1724c28 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 20 Jun 2024 09:47:21 -0400 Subject: [PATCH 04/10] chore: Linter and PHPStan compliance met --- includes/utils/class-ql-session-handler.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index 786da4d5..8197946a 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -406,9 +406,12 @@ function ( $headers ) { /** * {@inheritDoc} + * + * @return void */ public function set_customer_session_cookie( $set ) { parent::set_customer_session_cookie( $set ); + if ( $set ) { $this->_issuing_new_cookie = true; } From a66fbbc10ab040d22a6f0ffd461f49af98bae0d5 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 20 Jun 2024 12:03:52 -0400 Subject: [PATCH 05/10] fix: More cart session save triggered implemented --- access-functions.php | 10 +++------- composer.lock | 18 +++++++++--------- includes/mutation/class-cart-add-fee.php | 2 ++ includes/mutation/class-cart-apply-coupon.php | 2 ++ includes/mutation/class-cart-empty.php | 2 ++ includes/mutation/class-cart-fill.php | 2 ++ .../mutation/class-cart-remove-coupons.php | 2 ++ includes/mutation/class-cart-remove-items.php | 2 ++ includes/mutation/class-cart-restore-items.php | 2 ++ .../class-cart-update-item-quantities.php | 3 +++ .../class-cart-update-shipping-method.php | 2 ++ includes/mutation/class-customer-update.php | 4 ++++ includes/type/object/class-root-query.php | 2 +- includes/utils/class-ql-session-handler.php | 11 +++++++++++ .../class-session-transaction-manager.php | 7 +++++++ tests/functional/CartTransactionQueueCest.php | 4 ++-- .../firebase/php-jwt/src/CachedKeySet.php | 14 ++++---------- vendor-prefixed/firebase/php-jwt/src/JWT.php | 3 --- 18 files changed, 60 insertions(+), 32 deletions(-) diff --git a/access-functions.php b/access-functions.php index a67a869d..e4de7f40 100644 --- a/access-functions.php +++ b/access-functions.php @@ -19,8 +19,7 @@ * @return bool - True if $haystack starts with $needle, false otherwise. */ function str_starts_with( $haystack, $needle ) { - $length = strlen( $needle ); - return ( substr( $haystack, 0, $length ) === $needle ); + return 0 === strpos( $haystack, $needle ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.str_starts_with } } @@ -38,11 +37,8 @@ function str_starts_with( $haystack, $needle ) { */ function str_ends_with( $haystack, $needle ) { $length = strlen( $needle ); - if ( 0 === $length ) { - return true; - } - - return ( substr( $haystack, -$length ) === $needle ); + return $length === 0 + || $length - 1 === strpos( $haystack, $needle, - $length ); } }//end if diff --git a/composer.lock b/composer.lock index 70d06917..42be2968 100644 --- a/composer.lock +++ b/composer.lock @@ -8,26 +8,26 @@ "packages": [ { "name": "firebase/php-jwt", - "version": "v6.10.1", + "version": "v6.10.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "500501c2ce893c824c801da135d02661199f60c5" + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", - "reference": "500501c2ce893c824c801da135d02661199f60c5", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", "shasum": "" }, "require": { - "php": "^8.0" + "php": "^7.4||^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/guzzle": "^6.5||^7.4", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psr/cache": "^2.0||^3.0", + "psr/cache": "^1.0||^2.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, @@ -65,9 +65,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" }, - "time": "2024-05-18T18:05:11+00:00" + "time": "2023-12-01T16:26:39+00:00" } ], "packages-dev": [ diff --git a/includes/mutation/class-cart-add-fee.php b/includes/mutation/class-cart-add-fee.php index 41a08b39..a986eb79 100644 --- a/includes/mutation/class-cart-add-fee.php +++ b/includes/mutation/class-cart-add-fee.php @@ -106,6 +106,8 @@ public static function mutate_and_get_payload() { // Add cart fee. \WC()->cart->add_fee( ...$cart_fee_args ); + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'id' => \sanitize_title( $input['name'] ) ]; }; diff --git a/includes/mutation/class-cart-apply-coupon.php b/includes/mutation/class-cart-apply-coupon.php index dbd12a17..2e1b5f6a 100644 --- a/includes/mutation/class-cart-apply-coupon.php +++ b/includes/mutation/class-cart-apply-coupon.php @@ -76,6 +76,8 @@ public static function mutate_and_get_payload() { $reason = ''; // If validate and successful applied to cart, return payload. if ( Cart_Mutation::validate_coupon( $input['code'], $reason ) && \WC()->cart->apply_coupon( $input['code'] ) ) { + do_action( 'woographql_update_session', true ); + return [ 'code' => $input['code'] ]; } diff --git a/includes/mutation/class-cart-empty.php b/includes/mutation/class-cart-empty.php index 5aef5464..3d634d40 100644 --- a/includes/mutation/class-cart-empty.php +++ b/includes/mutation/class-cart-empty.php @@ -94,6 +94,8 @@ public static function mutate_and_get_payload() { */ do_action( 'graphql_woocommerce_after_empty_cart', $cloned_cart, $input, $context, $info ); + do_action( 'woographql_update_session', true ); + return [ 'cart' => $cloned_cart ]; }; } diff --git a/includes/mutation/class-cart-fill.php b/includes/mutation/class-cart-fill.php index ae767bce..9df21193 100644 --- a/includes/mutation/class-cart-fill.php +++ b/includes/mutation/class-cart-fill.php @@ -270,6 +270,8 @@ public static function mutate_and_get_payload() { // Recalculate totals. \WC()->cart->calculate_totals(); + do_action( 'woographql_update_session', true ); + // Return payload. return compact( 'added', diff --git a/includes/mutation/class-cart-remove-coupons.php b/includes/mutation/class-cart-remove-coupons.php index 9ebf2813..6be66f5f 100644 --- a/includes/mutation/class-cart-remove-coupons.php +++ b/includes/mutation/class-cart-remove-coupons.php @@ -86,6 +86,8 @@ public static function mutate_and_get_payload() { } } + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'cart' => \WC()->cart ]; }; diff --git a/includes/mutation/class-cart-remove-items.php b/includes/mutation/class-cart-remove-items.php index 0a0b265d..1c03dd39 100644 --- a/includes/mutation/class-cart-remove-items.php +++ b/includes/mutation/class-cart-remove-items.php @@ -96,6 +96,8 @@ public static function mutate_and_get_payload() { } } + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'items' => $cart_items ]; }; diff --git a/includes/mutation/class-cart-restore-items.php b/includes/mutation/class-cart-restore-items.php index 2f6979a1..aa57476a 100644 --- a/includes/mutation/class-cart-restore-items.php +++ b/includes/mutation/class-cart-restore-items.php @@ -82,6 +82,8 @@ public static function mutate_and_get_payload() { $cart_items = Cart_Mutation::retrieve_cart_items( $input, $context, $info, 'restore' ); + do_action( 'woographql_update_session', true ); + // Return payload. return [ 'items' => $cart_items ]; }; diff --git a/includes/mutation/class-cart-update-item-quantities.php b/includes/mutation/class-cart-update-item-quantities.php index 1f455b48..f9d85960 100644 --- a/includes/mutation/class-cart-update-item-quantities.php +++ b/includes/mutation/class-cart-update-item-quantities.php @@ -165,6 +165,9 @@ static function ( $value ) { $info ); + + do_action( 'woographql_update_session', true ); + return [ 'removed' => $removed_items, 'updated' => array_keys( $updated ), diff --git a/includes/mutation/class-cart-update-shipping-method.php b/includes/mutation/class-cart-update-shipping-method.php index cf44f0db..4a897a8d 100644 --- a/includes/mutation/class-cart-update-shipping-method.php +++ b/includes/mutation/class-cart-update-shipping-method.php @@ -78,6 +78,8 @@ public static function mutate_and_get_payload() { // Recalculate totals. \WC()->cart->calculate_totals(); + do_action( 'woographql_update_session', true ); + return []; }; } diff --git a/includes/mutation/class-customer-update.php b/includes/mutation/class-customer-update.php index 48466cd2..f7e49f65 100644 --- a/includes/mutation/class-customer-update.php +++ b/includes/mutation/class-customer-update.php @@ -153,6 +153,10 @@ public static function mutate_and_get_payload() { // Save customer and get customer ID. $customer->save(); + if ( $session_only ) { + do_action( 'woographql_update_session', true ); + } + // Return payload. return ! empty( $payload ) ? $payload : [ 'id' => 'session' ]; }; diff --git a/includes/type/object/class-root-query.php b/includes/type/object/class-root-query.php index 31dd1bea..a2d6a60d 100644 --- a/includes/type/object/class-root-query.php +++ b/includes/type/object/class-root-query.php @@ -44,7 +44,7 @@ public static function register_fields() { throw new UserError( $token_invalid ); } - $cart = Factory::resolve_cart(); + $cart = new \WC_Cart(); if ( ! empty( $args['recalculateTotals'] ) ) { $cart->calculate_totals(); } diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index 8197946a..ea4f884e 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -128,6 +128,17 @@ public function init() { } } + /** + * Mark the session as dirty. + * + * To trigger a save of the session data. + * + * @return void + */ + public function mark_dirty() { + $this->_dirty = true; + } + /** * Setup token and customer ID. * diff --git a/includes/utils/class-session-transaction-manager.php b/includes/utils/class-session-transaction-manager.php index cc4fe9fb..534b81cf 100644 --- a/includes/utils/class-session-transaction-manager.php +++ b/includes/utils/class-session-transaction-manager.php @@ -61,7 +61,14 @@ public function __construct( &$session_handler ) { add_action( 'graphql_before_resolve_field', [ $this, 'update_transaction_queue' ], 10, 4 ); add_action( 'graphql_mutation_response', [ $this, 'pop_transaction_id' ], 20, 6 ); + add_action( 'woographql_session_transaction_complete', [ $this->session_handler, 'save_if_dirty' ], 10 ); + + add_action( 'woocommerce_add_to_cart', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_item_removed', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_item_restored', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_item_set_quantity', [ $this->session_handler, 'mark_dirty' ] ); + add_action( 'woocommerce_cart_emptied', [ $this->session_handler, 'mark_dirty' ] ); } /** diff --git a/tests/functional/CartTransactionQueueCest.php b/tests/functional/CartTransactionQueueCest.php index d6f730f7..9e3d26c6 100644 --- a/tests/functional/CartTransactionQueueCest.php +++ b/tests/functional/CartTransactionQueueCest.php @@ -96,12 +96,12 @@ public function _startAuthenticatedSession( $I ) { ); // Retrieve JWT Authorization Token for later use. - $auth_token = $I->lodashGet( $success, 'login.authToken' ); + $auth_token = $I->lodashGet( $success, 'data.login.authToken' ); // Retrieve session token. Add as "Session %s" in the woocommerce-session HTTP header to future requests // so WooCommerce can identify the user session associated with actions made in the GraphQL requests. // You can also retrieve the token from the "woocommerce-session" HTTP response header. - $initial_session_token = $I->lodashGet( $success, 'login.sessionToken' ); + $initial_session_token = $I->lodashGet( $success, 'data.login.sessionToken' ); $headers = [ 'Authorization' => "Bearer {$auth_token}", diff --git a/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php b/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php index b99b2af9..cecb0c5a 100644 --- a/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php +++ b/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php @@ -218,21 +218,15 @@ private function rateLimitExceeded(): bool } $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); - - $cacheItemData = []; - if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) { - $cacheItemData = $data; + if (!$cacheItem->isHit()) { + $cacheItem->expiresAfter(1); // # of calls are cached each minute } - $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0; - $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC')); - + $callsPerMinute = (int) $cacheItem->get(); if (++$callsPerMinute > $this->maxCallsPerMinute) { return true; } - - $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]); - $cacheItem->expiresAt($expiry); + $cacheItem->set($callsPerMinute); $this->cache->save($cacheItem); return false; } diff --git a/vendor-prefixed/firebase/php-jwt/src/JWT.php b/vendor-prefixed/firebase/php-jwt/src/JWT.php index e2eb7091..82fa11d1 100644 --- a/vendor-prefixed/firebase/php-jwt/src/JWT.php +++ b/vendor-prefixed/firebase/php-jwt/src/JWT.php @@ -257,9 +257,6 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - if (!\is_resource($key) && !openssl_pkey_get_private($key)) { - throw new DomainException('OpenSSL unable to validate key'); - } $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException('OpenSSL unable to sign data'); From d09949c320cf7374940fc39df8868495e87e9f67 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 20 Jun 2024 12:41:29 -0400 Subject: [PATCH 06/10] fix: More cart session save triggered implemented --- composer.lock | 18 +++++++++--------- includes/type/object/class-root-query.php | 2 +- .../firebase/php-jwt/src/CachedKeySet.php | 14 ++++++++++---- vendor-prefixed/firebase/php-jwt/src/JWT.php | 3 +++ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/composer.lock b/composer.lock index 42be2968..70d06917 100644 --- a/composer.lock +++ b/composer.lock @@ -8,26 +8,26 @@ "packages": [ { "name": "firebase/php-jwt", - "version": "v6.10.0", + "version": "v6.10.1", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + "reference": "500501c2ce893c824c801da135d02661199f60c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", "shasum": "" }, "require": { - "php": "^7.4||^8.0" + "php": "^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", + "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", + "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, @@ -65,9 +65,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" }, - "time": "2023-12-01T16:26:39+00:00" + "time": "2024-05-18T18:05:11+00:00" } ], "packages-dev": [ diff --git a/includes/type/object/class-root-query.php b/includes/type/object/class-root-query.php index a2d6a60d..31dd1bea 100644 --- a/includes/type/object/class-root-query.php +++ b/includes/type/object/class-root-query.php @@ -44,7 +44,7 @@ public static function register_fields() { throw new UserError( $token_invalid ); } - $cart = new \WC_Cart(); + $cart = Factory::resolve_cart(); if ( ! empty( $args['recalculateTotals'] ) ) { $cart->calculate_totals(); } diff --git a/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php b/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php index cecb0c5a..b99b2af9 100644 --- a/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php +++ b/vendor-prefixed/firebase/php-jwt/src/CachedKeySet.php @@ -218,15 +218,21 @@ private function rateLimitExceeded(): bool } $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); - if (!$cacheItem->isHit()) { - $cacheItem->expiresAfter(1); // # of calls are cached each minute + + $cacheItemData = []; + if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) { + $cacheItemData = $data; } - $callsPerMinute = (int) $cacheItem->get(); + $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0; + $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC')); + if (++$callsPerMinute > $this->maxCallsPerMinute) { return true; } - $cacheItem->set($callsPerMinute); + + $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]); + $cacheItem->expiresAt($expiry); $this->cache->save($cacheItem); return false; } diff --git a/vendor-prefixed/firebase/php-jwt/src/JWT.php b/vendor-prefixed/firebase/php-jwt/src/JWT.php index 82fa11d1..e2eb7091 100644 --- a/vendor-prefixed/firebase/php-jwt/src/JWT.php +++ b/vendor-prefixed/firebase/php-jwt/src/JWT.php @@ -257,6 +257,9 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; + if (!\is_resource($key) && !openssl_pkey_get_private($key)) { + throw new DomainException('OpenSSL unable to validate key'); + } $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException('OpenSSL unable to sign data'); From 760d52da0df480d921f60a79b6a4b31c416a067e Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 20 Jun 2024 12:50:35 -0400 Subject: [PATCH 07/10] chore: Linter compliance met --- includes/mutation/class-cart-apply-coupon.php | 2 +- includes/mutation/class-cart-update-item-quantities.php | 1 - includes/utils/class-ql-session-handler.php | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/includes/mutation/class-cart-apply-coupon.php b/includes/mutation/class-cart-apply-coupon.php index 2e1b5f6a..b136d41d 100644 --- a/includes/mutation/class-cart-apply-coupon.php +++ b/includes/mutation/class-cart-apply-coupon.php @@ -77,7 +77,7 @@ public static function mutate_and_get_payload() { // If validate and successful applied to cart, return payload. if ( Cart_Mutation::validate_coupon( $input['code'], $reason ) && \WC()->cart->apply_coupon( $input['code'] ) ) { do_action( 'woographql_update_session', true ); - + return [ 'code' => $input['code'] ]; } diff --git a/includes/mutation/class-cart-update-item-quantities.php b/includes/mutation/class-cart-update-item-quantities.php index f9d85960..a8521998 100644 --- a/includes/mutation/class-cart-update-item-quantities.php +++ b/includes/mutation/class-cart-update-item-quantities.php @@ -165,7 +165,6 @@ static function ( $value ) { $info ); - do_action( 'woographql_update_session', true ); return [ diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index ea4f884e..a8d0525d 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -130,7 +130,7 @@ public function init() { /** * Mark the session as dirty. - * + * * To trigger a save of the session data. * * @return void @@ -417,12 +417,12 @@ function ( $headers ) { /** * {@inheritDoc} - * + * * @return void */ public function set_customer_session_cookie( $set ) { parent::set_customer_session_cookie( $set ); - + if ( $set ) { $this->_issuing_new_cookie = true; } From 0a9408f5fe238b376c8cc131730589cdfcdcdbfa Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 20 Jun 2024 12:53:09 -0400 Subject: [PATCH 08/10] chore: Linter compliance met --- access-functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/access-functions.php b/access-functions.php index e4de7f40..d86529e7 100644 --- a/access-functions.php +++ b/access-functions.php @@ -37,8 +37,8 @@ function str_starts_with( $haystack, $needle ) { */ function str_ends_with( $haystack, $needle ) { $length = strlen( $needle ); - return $length === 0 - || $length - 1 === strpos( $haystack, $needle, - $length ); + return 0 === $length + || strpos( $haystack, $needle, - $length ) === $length - 1; } }//end if From 3d17e698d942e82ad50fd39c2900a269cbcb049e Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 7 Aug 2024 12:27:19 -0400 Subject: [PATCH 09/10] feat: forgetSession mutation added --- codeception.dist.yml | 4 - includes/class-type-registry.php | 3 +- includes/class-wp-graphql-woocommerce.php | 3 +- includes/mutation/class-session-delete.php | 95 +++++++++++++++++++ ...e-session.php => class-session-update.php} | 4 +- includes/utils/class-ql-session-handler.php | 1 + tests/_support/Factory/CartFactory.php | 4 +- ...ationTest.php => SessionMutationsTest.php} | 71 +++++++++++++- 8 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 includes/mutation/class-session-delete.php rename includes/mutation/{class-update-session.php => class-session-update.php} (98%) rename tests/wpunit/{UpdateSessionMutationTest.php => SessionMutationsTest.php} (50%) diff --git a/codeception.dist.yml b/codeception.dist.yml index 14191bf3..283cbaf9 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -71,10 +71,6 @@ modules: dbUser: '%DB_USER%' dbPassword: '%DB_PASSWORD%' dbUrl: 'mysql://%DB_USER%:%DB_PASSWORD%@%DB_HOST%:%DB_PORT%/%DB_NAME%' - dbName: '%DB_NAME%' - dbHost: '%DB_HOST%' - dbUser: '%DB_USER%' - dbPassword: '%DB_PASSWORD%' tablePrefix: '%WP_TABLE_PREFIX%' domain: '%WORDPRESS_DOMAIN%' adminEmail: '%ADMIN_EMAIL%' diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index 72628eea..e7c41db8 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -199,6 +199,7 @@ public function init() { Mutation\Tax_Rate_Create::register_mutation(); Mutation\Tax_Rate_Delete::register_mutation(); Mutation\Tax_Rate_Update::register_mutation(); - Mutation\Update_Session::register_mutation(); + Mutation\Session_Delete::register_mutation(); + Mutation\Session_Update::register_mutation(); } } diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index 6ae6c645..bc284a1b 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -343,6 +343,8 @@ private function includes() { require $include_directory_path . 'mutation/class-review-update.php'; require $include_directory_path . 'mutation/class-payment-method-delete.php'; require $include_directory_path . 'mutation/class-payment-method-set-default.php'; + require $include_directory_path . 'mutation/class-session-delete.php'; + require $include_directory_path . 'mutation/class-session-update.php'; require $include_directory_path . 'mutation/class-shipping-zone-create.php'; require $include_directory_path . 'mutation/class-shipping-zone-delete.php'; require $include_directory_path . 'mutation/class-shipping-zone-locations-clear.php'; @@ -356,7 +358,6 @@ private function includes() { require $include_directory_path . 'mutation/class-tax-rate-create.php'; require $include_directory_path . 'mutation/class-tax-rate-delete.php'; require $include_directory_path . 'mutation/class-tax-rate-update.php'; - require $include_directory_path . 'mutation/class-update-session.php'; // Include connection class/function files. require $include_directory_path . 'connection/wc-cpt-connection-args.php'; diff --git a/includes/mutation/class-session-delete.php b/includes/mutation/class-session-delete.php new file mode 100644 index 00000000..cceafab2 --- /dev/null +++ b/includes/mutation/class-session-delete.php @@ -0,0 +1,95 @@ + [], + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'session' => [ + 'type' => [ 'list_of' => 'MetaData' ], + 'resolve' => static function ( $payload ) { + // Guard against missing session data. + if ( empty( $payload['session'] ) ) { + return []; + } + + // Prepare session data. + $session = []; + foreach ( $payload['session'] as $key => $value ) { + $meta = new \stdClass(); + $meta->id = null; + $meta->key = $key; + $meta->value = maybe_unserialize( $value ); + $session[] = $meta; + } + + return $session; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input ) { + Cart_Mutation::check_session_token(); + + /** + * Session handler. + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; + + // Get session data. + $session_data = $session->get_session_data(); + do_action( 'woographql_before_forget_session', $session_data, $input, $session ); + + // Clear session data. + $session->forget_session(); + + do_action( 'woographql_after_forget_session', $session_data, $input, $session ); + + // Return payload. + return [ 'session' => $session_data ]; + }; + } +} diff --git a/includes/mutation/class-update-session.php b/includes/mutation/class-session-update.php similarity index 98% rename from includes/mutation/class-update-session.php rename to includes/mutation/class-session-update.php index b335c972..a1040be6 100644 --- a/includes/mutation/class-update-session.php +++ b/includes/mutation/class-session-update.php @@ -15,9 +15,9 @@ use WPGraphQL\WooCommerce\Model\Customer; /** - * Class - Update_Session + * Class - Session_Update */ -class Update_Session { +class Session_Update { /** * Registers mutation * diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index a8d0525d..490a41c6 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -200,6 +200,7 @@ static function ( $errors ) use ( $token ) { return; } + // Distribute new session token on GraphQL requests, otherwise distribute a new session cookie. if ( Router::is_graphql_http_request() ) { // Start new session. $this->set_session_expiration(); diff --git a/tests/_support/Factory/CartFactory.php b/tests/_support/Factory/CartFactory.php index cde85bc7..15dc7bc3 100644 --- a/tests/_support/Factory/CartFactory.php +++ b/tests/_support/Factory/CartFactory.php @@ -15,9 +15,9 @@ class CartFactory { /** * Add products to the cart. * - * @param array ...$products Product to be added to the cart. + * @param array> ...$products Product to be added to the cart. * - * @return array + * @return array */ public function add( ...$products ) { $keys = []; diff --git a/tests/wpunit/UpdateSessionMutationTest.php b/tests/wpunit/SessionMutationsTest.php similarity index 50% rename from tests/wpunit/UpdateSessionMutationTest.php rename to tests/wpunit/SessionMutationsTest.php index bd48d401..b12d1ea7 100644 --- a/tests/wpunit/UpdateSessionMutationTest.php +++ b/tests/wpunit/SessionMutationsTest.php @@ -1,6 +1,6 @@ factory->customer->create(); @@ -62,4 +62,71 @@ public function testUpdateSessionMutation() { $this->assertQuerySuccessful( $response, $expected ); } -} + + public function testForgetSessionMutation() { + // Create registered customer. + $registered = $this->factory->customer->create(); + $this->loginAs( $registered ); + + // Add products to cart. + $this->factory->cart->add( + [ + 'product_id' => $this->factory->product->createSimple(), + 'quantity' => 2, + ], + [ + 'product_id' => $this->factory->product->createSimple(), + 'quantity' => 1, + ] + ); + + // Save session. + \WC()->session->save_data(); + + // Reinitialize session. + \WC()->session->init(); + + // Confirm cart has items. + $cart_query = ' + query { + cart { + contents { + nodes { + key + } + } + } + } + '; + + $response = $this->graphql( [ 'query' => $cart_query ] ); + $this->assertQuerySuccessful( + $response, + [ $this->expectedField( 'cart.contents.nodes', static::NOT_FALSY ) ] + ); + + // Forget session. + $query = 'mutation { + forgetSession(input: {}) { + session { + id + key + value + } + } + }'; + + $response = $this->graphql( compact( 'query' ) ); + $this->assertQuerySuccessful( $response ); + + // Reinitialize session. + \WC()->session->init(); + + // Confirm cart is empty. + $response = $this->graphql( [ 'query' => $cart_query ] ); + $this->assertQuerySuccessful( + $response, + [ $this->expectedField( 'cart.contents.nodes', static::IS_FALSY ) ] + ); + } +} \ No newline at end of file From 5fbefb1a56d5a547f1fbe672c1ce95a2dc8624fb Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 7 Aug 2024 12:32:01 -0400 Subject: [PATCH 10/10] feat: forgetSession mutation added --- includes/utils/class-session-transaction-manager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/utils/class-session-transaction-manager.php b/includes/utils/class-session-transaction-manager.php index 534b81cf..be6b9d56 100644 --- a/includes/utils/class-session-transaction-manager.php +++ b/includes/utils/class-session-transaction-manager.php @@ -107,6 +107,7 @@ public static function get_session_mutations() { 'updateShippingMethod', 'updateCustomer', 'updateSession', + 'forgetSession', ] ); }