Skip to content

Commit

Permalink
Improve merchant session request with preloaded data (#8645)
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarcosta99 authored Apr 24, 2024
1 parent 8732356 commit 9b6d54c
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 25 deletions.
4 changes: 4 additions & 0 deletions changelog/fix-2614-woopay-session-preload-requests
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fix

Improve merchant session request with preloaded data.
66 changes: 53 additions & 13 deletions includes/admin/class-wc-rest-woopay-session-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

use WCPay\WooPay\WooPay_Session;
use Automattic\Jetpack\Connection\Rest_Authentication;
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
use WCPay\Exceptions\Rest_Request_Exception;
use WCPay\Logger;

/**
* REST controller to check get WooPay extension data for user.
Expand Down Expand Up @@ -49,20 +52,32 @@ public function register_routes() {
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_Error|WP_REST_Response
* @return WP_Error|WP_REST_Response The initial session request data.
*/
public function get_session_data( WP_REST_Request $request ): WP_REST_Response {
// phpcs:ignore
/**
* @psalm-suppress UndefinedClass
*/
$response = WooPay_Session::get_init_session_request();
// This was needed as the preloaded requests were not honoring the cart token and so were empty carts.
// It would be ideal to get this to successfully preload the cart data so WooPay doesn't need to make
// a separate request to get the cart data.
unset( $response['preloaded_requests'] );

return rest_ensure_response( $response );
try {
$payload = $this->validated_cart_token_payload( $request->get_header( 'cart_token' ) );
$user_id = (int) $payload->user_id ?? null;

if ( is_int( $user_id ) && $user_id > 0 ) {
wp_set_current_user( $user_id );
}

// phpcs:ignore
/**
* @psalm-suppress UndefinedClass
*/
$response = WooPay_Session::get_init_session_request( null, null, null, $request );

return rest_ensure_response( $response );
} catch ( Rest_Request_Exception $e ) {
$error_code = $e->getCode() === 400 ? 'rest_invalid_param' : 'wcpay_server_error';
$error = new WP_Error( $error_code, $e->getMessage(), [ 'status' => $e->getCode() ] );

Logger::log( 'Error validating cart token from WooPay request: ' . $e->getMessage() );

return rest_convert_error_to_response( $error );
}
}

/**
Expand All @@ -74,12 +89,37 @@ public function check_permission() {
return $this->is_request_from_woopay() && $this->has_valid_request_signature();
}

/**
* Validates the cart token and returns its payload.
*
* @param string|null $cart_token The cart token to validate.
*
* @return object The validated cart token.
*
* @throws Rest_Request_Exception If the cart token is invalid, missing, or cannot be validated.
*/
public function validated_cart_token_payload( $cart_token ): object {
if ( ! $cart_token ) {
throw new Rest_Request_Exception( 'Missing cart token.', 400 );
}

if ( ! class_exists( JsonWebToken::class ) ) {
throw new Rest_Request_Exception( 'Cannot validate cart token.', 500 );
}

if ( ! JsonWebToken::validate( $cart_token, '@' . wp_salt() ) ) {
throw new Rest_Request_Exception( 'Invalid cart token.', 400 );
}

return JsonWebToken::get_parts( $cart_token )->payload;
}

/**
* Returns true if the request that's currently being processed is signed with the blog token.
*
* @return bool True if the request signature is valid.
*/
private function has_valid_request_signature() {
private function has_valid_request_signature(): bool {
return apply_filters( 'wcpay_woopay_is_signed_with_blog_token', Rest_Authentication::is_signed_with_blog_token() );
}

Expand Down
73 changes: 61 additions & 12 deletions includes/woopay/class-woopay-session.php
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,66 @@ public static function get_frontend_init_session_request() {
return WooPay_Utilities::encrypt_and_sign_data( $session );
}

/**
* Retrieves cart data from the current session.
*
* If the request doesn't come from WooPay, this uses the same strategy in
* `hydrate_from_api` on the Checkout Block to retrieve cart data.
*
* @param int|null $order_id Pay-for-order order ID.
* @param string|null $key Pay-for-order key.
* @param string|null $billing_email Pay-for-order billing email.
* @param WP_REST_Request|null $woopay_request The WooPay request object.
* @return array The cart data.
*/
private static function get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request ) {
if ( ! $woopay_request ) {
return ! $is_pay_for_order
? rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body']
: rest_preload_api_request( [], "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) )[ "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) ]['body'];
}

$cart_request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' );
$cart_request->set_header( 'Cart-Token', $woopay_request->get_header('cart_token') );
return rest_do_request( $cart_request )->get_data();
}

/**
* Retrieves checkout data from the current session.
*
* If the request doesn't come from WooPay, this uses the same strategy in
* `hydrate_from_api` on the Checkout Block to retrieve checkout data.
*
* @param WP_REST_Request $woopay_request The WooPay request object.
* @return mixed The checkout data.
*/
private static function get_checkout_data( $woopay_request ) {
add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );

if ( ! $woopay_request ) {
$preloaded_checkout_data = rest_preload_api_request( [], '/wc/store/v1/checkout' );
$checkout_data = isset( $preloaded_checkout_data['/wc/store/v1/checkout'] ) ? $preloaded_checkout_data['/wc/store/v1/checkout']['body'] : '';
} else {
$checkout_request = new WP_REST_Request( 'GET', '/wc/store/v1/checkout' );
$checkout_request->set_header( 'Cart-Token', $woopay_request->get_header('cart_token') );
$checkout_data = rest_do_request( $checkout_request )->get_data();
}

remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );

return $checkout_data;
}

/**
* Returns the initial session request data.
*
* @param int|null $order_id Pay-for-order order ID.
* @param int|null $order_id Pay-for-order order ID.
* @param string|null $key Pay-for-order key.
* @param string|null $billing_email Pay-for-order billing email.
* @param WP_REST_Request|null $woopay_request The WooPay request object.
* @return array The initial session request data without email and user_session.
*/
public static function get_init_session_request( $order_id = null, $key = null, $billing_email = null ) {
public static function get_init_session_request( $order_id = null, $key = null, $billing_email = null, $woopay_request = null ) {
$user = wp_get_current_user();
$is_pay_for_order = null !== $order_id;
$order = wc_get_order( $order_id );
Expand Down Expand Up @@ -381,14 +432,12 @@ public static function get_init_session_request( $order_id = null, $key = null,
include_once WCPAY_ABSPATH . 'includes/compat/blocks/class-blocks-data-extractor.php';
$blocks_data_extractor = new Blocks_Data_Extractor();

// This uses the same logic as the Checkout block in hydrate_from_api to get the cart and checkout data.
$cart_data = ! $is_pay_for_order
? rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body']
: rest_preload_api_request( [], "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) )[ "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) ]['body'];
add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
$preloaded_checkout_data = rest_preload_api_request( [], '/wc/store/v1/checkout' );
remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
$checkout_data = isset( $preloaded_checkout_data['/wc/store/v1/checkout'] ) ? $preloaded_checkout_data['/wc/store/v1/checkout']['body'] : '';
$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 );

if ( $woopay_request ) {
$order_id = $checkout_data['order_id'] ?? null;
}

$email = ! empty( $_POST['email'] ) ? wc_clean( wp_unslash( $_POST['email'] ) ) : '';

Expand Down Expand Up @@ -560,14 +609,14 @@ public static function ajax_get_woopay_minimum_session_data() {

/**
* Return WooPay minimum session data.
*
*
* @return array Array of minimum session data used by WooPay or false on failures.
*/
public static function get_woopay_minimum_session_data() {
if ( ! WC_Payments_Features::is_client_secret_encryption_eligible() ) {
return [];
}

$blog_id = Jetpack_Options::get_option('id');
if ( empty( $blog_id ) ) {
return [];
Expand Down

0 comments on commit 9b6d54c

Please sign in to comment.