From 25db37e303f2a47664e88b2526457be567dd14d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <10233985+cesarcosta99@users.noreply.github.com> Date: Fri, 14 Jun 2024 19:41:54 -0300 Subject: [PATCH] Add support for adapted extensions in Direct Checkout (#8849) --- .../add-2648-adapted-extensions-compatibility | 4 +++ .../checkout/woopay/connect/user-connect.js | 16 +++++++++ .../woopay/connect/woopay-connect-iframe.js | 4 ++- .../direct-checkout/woopay-direct-checkout.js | 13 ++++++- ...lass-wc-rest-woopay-session-controller.php | 7 ++++ includes/class-wc-payments-features.php | 2 +- includes/class-wc-payments.php | 1 + includes/woopay/class-woopay-session.php | 35 ++++++++++++++++-- includes/woopay/class-woopay-utilities.php | 36 +++++++++++++++++++ .../unit/test-class-wc-payments-features.php | 7 ++-- 10 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 changelog/add-2648-adapted-extensions-compatibility diff --git a/changelog/add-2648-adapted-extensions-compatibility b/changelog/add-2648-adapted-extensions-compatibility new file mode 100644 index 00000000000..6f4dfabe3d4 --- /dev/null +++ b/changelog/add-2648-adapted-extensions-compatibility @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Enable adapted extensions compatibility with Direct Checkout. diff --git a/client/checkout/woopay/connect/user-connect.js b/client/checkout/woopay/connect/user-connect.js index 9e71772f086..ebae7e7ecf2 100644 --- a/client/checkout/woopay/connect/user-connect.js +++ b/client/checkout/woopay/connect/user-connect.js @@ -11,6 +11,7 @@ class WooPayUserConnect extends WoopayConnect { this.listeners = { ...this.listeners, getIsUserLoggedInCallback: () => {}, + getEncryptedDataCallback: () => {}, }; } @@ -26,6 +27,18 @@ class WooPayUserConnect extends WoopayConnect { ); } + /** + * Retrieves encrypted data from WooPay. + * + * @return {Promise} Resolves to an object with encrypted data. + */ + async getEncryptedData() { + return await this.sendMessageAndListenWith( + { action: 'getEncryptedData' }, + 'getEncryptedDataCallback' + ); + } + /** * Handles the callback from the WooPayConnectIframe. * @@ -38,6 +51,9 @@ class WooPayUserConnect extends WoopayConnect { case 'get_is_user_logged_in_success': this.listeners.getIsUserLoggedInCallback( data.value ); break; + case 'get_encrypted_data_success': + this.listeners.getEncryptedDataCallback( data.value ); + break; } } } diff --git a/client/checkout/woopay/connect/woopay-connect-iframe.js b/client/checkout/woopay/connect/woopay-connect-iframe.js index d87d24d8fdc..bfe6d783c11 100644 --- a/client/checkout/woopay/connect/woopay-connect-iframe.js +++ b/client/checkout/woopay/connect/woopay-connect-iframe.js @@ -22,9 +22,11 @@ export const WooPayConnectIframe = () => { const fetchConfigAndSetIframeUrl = async () => { const testMode = getConfig( 'testMode' ); const woopayHost = getConfig( 'woopayHost' ); + const blogId = getConfig( 'woopayMerchantId' ); const urlParams = new URLSearchParams( { testMode, - source_url: window.location.href, + source_url: window.location.href, // TODO: refactor this to camel case. + blogId, } ); const tracksUserId = await getTracksIdentity(); diff --git a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js index f52d5e03131..f969d6160c9 100644 --- a/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js +++ b/client/checkout/woopay/direct-checkout/woopay-direct-checkout.js @@ -81,12 +81,21 @@ class WooPayDirectCheckout { /** * Checks if the user is logged in. * - * @return {Promise<*>} Resolves to true if the user is logged in. + * @return {Promise} Resolves to true if the user is logged in. */ static async isUserLoggedIn() { return this.getUserConnect().isUserLoggedIn(); } + /** + * Retrieves encrypted data from WooPay. + * + * @return {Promise} Resolves to an object with encrypted data. + */ + static async getEncryptedData() { + return this.getUserConnect().getEncryptedData(); + } + /** * Checks if third-party cookies are enabled. * @@ -368,10 +377,12 @@ class WooPayDirectCheckout { * @return {Promise|*>} Resolves to the WooPay session response. */ static async getEncryptedSessionData() { + const encryptedData = await this.getEncryptedData(); return request( buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_session' ), { _ajax_nonce: getConfig( 'woopaySessionNonce' ), + ...( encryptedData && { encrypted_data: encryptedData } ), } ); } diff --git a/includes/admin/class-wc-rest-woopay-session-controller.php b/includes/admin/class-wc-rest-woopay-session-controller.php index e35f6e32f24..6cbdff4d9a0 100644 --- a/includes/admin/class-wc-rest-woopay-session-controller.php +++ b/includes/admin/class-wc-rest-woopay-session-controller.php @@ -41,6 +41,13 @@ public function register_routes() { 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_session_data' ], 'permission_callback' => [ $this, 'check_permission' ], + 'args' => [ + 'email' => [ + 'type' => 'string', + 'format' => 'email', + 'required' => true, + ], + ], ] ); } diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index 568ee184b5a..8e82ddb32f1 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -262,7 +262,7 @@ public static function is_woopay_direct_checkout_enabled() { $is_direct_checkout_eligible = is_array( $account_cache ) && ( $account_cache['platform_direct_checkout_eligible'] ?? false ); $is_direct_checkout_flag_enabled = '1' === get_option( self::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); - return $is_direct_checkout_eligible && $is_direct_checkout_flag_enabled && self::is_woopay_first_party_auth_enabled(); + return $is_direct_checkout_eligible && $is_direct_checkout_flag_enabled && self::is_woopay_eligible(); } /** diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 2a20910b787..f4f924408a5 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -1608,6 +1608,7 @@ public static function enqueue_woopay_common_config_script() { 'testMode' => $is_test_mode, 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'woopaySessionNonce' => wp_create_nonce( 'woopay_session_nonce' ), + 'woopayMerchantId' => Jetpack_Options::get_option( 'id' ), 'isWooPayDirectCheckoutEnabled' => WC_Payments_Features::is_woopay_direct_checkout_enabled(), 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index 11befdcc6cd..06fc2a93a35 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -377,6 +377,38 @@ private static function get_checkout_data( $woopay_request ) { return $checkout_data; } + /** + * Retrieves the user email from the current session. + * + * @param \WP_User $user The user object. + * @return string The user email. + */ + private static function get_user_email( $user ) { + if ( ! empty( $_POST['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + return sanitize_email( wp_unslash( $_POST['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification + } + + if ( ! empty( $_GET['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + return sanitize_email( wp_unslash( $_GET['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification + } + + if ( ! empty( $_POST['encrypted_data'] ) && is_array( $_POST['encrypted_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $decrypted_data = WooPay_Utilities::decrypt_signed_data( $_POST['encrypted_data'] ); + + if ( ! empty( $decrypted_data['user_email'] ) ) { + return sanitize_email( wp_unslash( $decrypted_data['user_email'] ) ); + } + } + + // As a last resort, we try to get the email from the customer logged in the store. + if ( $user->exists() ) { + return $user->user_email; + } + + return ''; + } + /** * Returns the initial session request data. * @@ -424,13 +456,12 @@ public static function get_init_session_request( $order_id = null, $key = null, $cart_data = self::get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request ); $checkout_data = self::get_checkout_data( $woopay_request ); + $email = self::get_user_email( $user ); if ( $woopay_request ) { $order_id = $checkout_data['order_id'] ?? null; } - $email = ! empty( $_POST['email'] ) ? wc_clean( wp_unslash( $_POST['email'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification - $request = [ 'wcpay_version' => WCPAY_VERSION_NUMBER, 'user_id' => $user->ID, diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php index 588285246cd..3c6116c2367 100644 --- a/includes/woopay/class-woopay-utilities.php +++ b/includes/woopay/class-woopay-utilities.php @@ -288,6 +288,42 @@ public static function encrypt_and_sign_data( $data ) { ]; } + /** + * Decode encrypted and signed data and return it. + * + * @param array $data The session, iv, and hash data for the encryption. + * @return mixed The decoded data. + */ + public static function decrypt_signed_data( $data ) { + $store_blog_token = ( self::get_woopay_url() === self::DEFAULT_WOOPAY_URL ) ? Jetpack_Options::get_option( 'blog_token' ) : 'dev_mode'; + + if ( empty( $store_blog_token ) ) { + return null; + } + + // Decode the data. + $decoded_data_request = array_map( 'base64_decode', $data ); + + // Verify the HMAC hash before decryption to ensure data integrity. + $computed_hash = hash_hmac( 'sha256', $decoded_data_request['iv'] . $decoded_data_request['data'], $store_blog_token ); + + // If the hashes don't match, the message may have been tampered with. + if ( ! hash_equals( $computed_hash, $decoded_data_request['hash'] ) ) { + return null; + } + + // Decipher the data using the blog token and the IV. + $decrypted_data = openssl_decrypt( $decoded_data_request['data'], 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $decoded_data_request['iv'] ); + + if ( false === $decrypted_data ) { + return null; + } + + $decrypted_data = json_decode( $decrypted_data, true ); + + return $decrypted_data; + } + /** * Get the persisted available countries. * diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php index a27ff2c432f..a8e61c36ad4 100644 --- a/tests/unit/test-class-wc-payments-features.php +++ b/tests/unit/test-class-wc-payments-features.php @@ -183,7 +183,6 @@ public function test_is_woopay_express_checkout_enabled_returns_false_when_woopa public function test_is_woopay_direct_checkout_enabled_returns_true() { $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); - $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); $this->mock_cache->method( 'get' )->willReturn( [ @@ -196,7 +195,6 @@ public function test_is_woopay_direct_checkout_enabled_returns_true() { public function test_is_woopay_direct_checkout_enabled_returns_false_when_flag_is_false() { $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); - $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '0' ); $this->mock_cache->method( 'get' )->willReturn( [ @@ -209,7 +207,6 @@ public function test_is_woopay_direct_checkout_enabled_returns_false_when_flag_i public function test_is_woopay_direct_checkout_enabled_returns_false_when_woopay_eligible_is_false() { $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); - $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); $this->mock_cache->method( 'get' )->willReturn( [ @@ -220,7 +217,7 @@ public function test_is_woopay_direct_checkout_enabled_returns_false_when_woopay $this->assertFalse( WC_Payments_Features::is_woopay_direct_checkout_enabled() ); } - public function test_is_woopay_direct_checkout_enabled_returns_false_when_first_party_auth_is_disabled() { + public function test_is_woopay_direct_checkout_enabled_returns_true_when_first_party_auth_is_disabled() { $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '0' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); @@ -230,7 +227,7 @@ public function test_is_woopay_direct_checkout_enabled_returns_false_when_first_ 'platform_direct_checkout_eligible' => true, ] ); - $this->assertFalse( WC_Payments_Features::is_woopay_direct_checkout_enabled() ); + $this->assertTrue( WC_Payments_Features::is_woopay_direct_checkout_enabled() ); } public function test_is_wcpay_frt_review_feature_active_returns_true() {