🧹 Remove reliance on the session in favor of passing the token directly

This commit is contained in:
Daniel Dudzic 2025-06-26 16:14:22 +02:00
parent ba7b01dfd6
commit 99cd8e4aeb
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
3 changed files with 140 additions and 238 deletions

View file

@ -153,26 +153,3 @@ class WP_HTML_Tag_Processor {
return '';
}
}
/**
* WooCommerce Session stubs for Psalm
*/
class WC_Session {
/**
* Get session cookie.
*
* @return string|false
*/
public function get_session_cookie() {
return '';
}
/**
* Set customer session cookie.
*
* @param bool $set Whether to set the cookie.
* @return void
*/
public function set_customer_session_cookie( $set ) {
}
}

View file

@ -245,59 +245,16 @@ class AxoGateway extends WC_Payment_Gateway {
);
}
// Check for stored 3DS errors.
$stored_error = $this->get_and_clear_stored_error();
if ( $stored_error ) {
return array(
'result' => 'failure',
'message' => $stored_error['message'],
);
// Check for tokens to determine if this is a 3DS return or initial payment.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$axo_nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$token_param = wc_clean( wp_unslash( $_GET['token'] ?? '' ) );
if ( empty( $axo_nonce ) && ! empty( $token_param ) ) {
return $this->process_3ds_return( $wc_order, $token_param );
}
$existing_order = $this->session_handler->order();
if ( $existing_order ) {
// Check if this session order belongs to current WC order and we're in 3DS context.
$session_order_belongs_to_current_wc_order = $this->session_order_matches_wc_order( $existing_order, $wc_order );
$is_3ds_context = $this->is_3ds_context();
if ( $session_order_belongs_to_current_wc_order && $is_3ds_context ) {
// This is a legitimate 3DS return for the current order.
try {
/**
* This filter controls if the method 'process()' from OrderProcessor will be called.
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process_captured_and_authorized( $wc_order, $existing_order );
}
// Clear session after successful processing.
$this->session_handler->destroy_session_data();
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
} catch ( Exception $exception ) {
// Handle 3DS processing failures with universal error approach.
// Clear session data since payment failed.
$this->session_handler->destroy_session_data();
return array(
'result' => 'failure',
'message' => $this->get_user_friendly_error_message( $exception ),
);
}
} else {
// Session order doesn't belong to current WC order OR not 3DS context.
$this->session_handler->destroy_session_data();
}
}
// No existing order or cleared session - this is an initial payment.
try {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) );
@ -308,31 +265,25 @@ class AxoGateway extends WC_Payment_Gateway {
}
// The `axo_nonce` is not a WP nonce, but a card-token generated by the JS SDK.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$token = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
// Enhanced token validation with universal error handling.
if ( empty( $token ) ) {
// Universal error return.
if ( empty( $axo_nonce ) ) {
return array(
'result' => 'failure',
'message' => $this->is_3ds_context()
? __( 'Payment session expired. Please try your payment again.', 'woocommerce-paypal-payments' )
: __( 'No payment token provided. Please try again.', 'woocommerce-paypal-payments' ),
'message' => __( 'No payment token provided. Please try again.', 'woocommerce-paypal-payments' ),
);
}
$order = $this->create_paypal_order( $wc_order, $token );
$order = $this->create_paypal_order( $wc_order, $axo_nonce );
// Check if 3DS verification is required.
$payer_action = $this->get_payer_action_url( $order );
// If 3DS verification is required, store order and redirect.
// If 3DS verification is required, redirect with token in return URL.
if ( $payer_action ) {
// Store the order in session before 3DS redirect.
$this->session_handler->replace_order( $order );
$return_url = home_url( WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) );
$return_url = add_query_arg(
'token',
$order->id(),
home_url( WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) )
);
$redirect_url = add_query_arg(
'redirect_uri',
@ -359,7 +310,8 @@ class AxoGateway extends WC_Payment_Gateway {
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
}
} catch ( Exception $exception ) {
// Error handling for initial payment failures.
// Error handling for payment failures.
$this->logger->error( '[AXO] Payment processing failed: ' . $exception->getMessage() );
return array(
'result' => 'failure',
'message' => $this->get_user_friendly_error_message( $exception ),
@ -375,20 +327,51 @@ class AxoGateway extends WC_Payment_Gateway {
}
/**
* Get and clear stored payment errors.
* Process 3DS return scenario.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param string $token The PayPal order token.
*
* @return array
*/
private function get_and_clear_stored_error() {
if ( ! WC()->session ) {
return null;
protected function process_3ds_return( WC_Order $wc_order, string $token ) : array {
try {
$paypal_order = $this->order_endpoint->order( $token );
if ( ! $paypal_order->status()->is( OrderStatus::COMPLETED ) ) {
return array(
'result' => 'failure',
'message' => __( '3D Secure authentication was not completed successfully. Please try again.', 'woocommerce-paypal-payments' ),
);
}
/**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process_captured_and_authorized( $wc_order, $paypal_order );
}
} catch ( Exception $exception ) {
$this->logger->error( '[AXO] 3DS return processing failed: ' . $exception->getMessage() );
return array(
'result' => 'failure',
'message' => $this->get_user_friendly_error_message( $exception ),
);
}
$stored_error = WC()->session->get( 'ppcp_payment_error' );
if ( $stored_error ) {
WC()->session->__unset( 'ppcp_payment_error' );
return $stored_error;
}
WC()->cart->empty_cart();
return null;
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
}
/**
@ -423,56 +406,21 @@ class AxoGateway extends WC_Payment_Gateway {
* Extract payer action URL from PayPal order.
*/
private function get_payer_action_url( Order $order ) {
foreach ( $order->links() as $link ) {
if ( $link->rel === 'payer-action' ) {
return $link->href;
$links = $order->links();
if ( ! $links ) {
return '';
}
foreach ( $links as $link ) {
if ( isset( $link->rel ) && $link->rel === 'payer-action' ) {
return $link->href ?? '';
}
}
return '';
}
/**
* Check if session order belongs to current WC order.
*
* @param Order $paypal_order The PayPal order from session.
* @param WC_Order $wc_order The current WooCommerce order.
* @return bool
*/
private function session_order_matches_wc_order( Order $paypal_order, WC_Order $wc_order ): bool {
$paypal_custom_id = $paypal_order->purchase_units()[0]->custom_id() ?? '';
$wc_order_id = (string) $wc_order->get_id();
return $paypal_custom_id === $wc_order_id;
}
/**
* Check if we're in a 3DS return context.
*
* @return bool
*/
private function is_3ds_context(): bool {
// Check referer for PayPal.
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if ( strpos( $referer, 'paypal.com' ) !== false ) {
return true;
}
// Check for 3DS-specific parameters.
$three_ds_params = array( 'liability_shift', 'state', 'code', 'authentication_state' );
foreach ( $three_ds_params as $param ) {
if ( isset( $_GET[ $param ] ) ) {
return true;
}
}
// Check if we're being called by ReturnUrlEndpoint.
if ( isset( $_GET['wc-ajax'] ) && $_GET['wc-ajax'] === 'ppc-return-url' ) {
return true;
}
return false;
}
/**
* Create a new PayPal order from the existing WC_Order instance.
*
@ -505,7 +453,8 @@ class AxoGateway extends WC_Payment_Gateway {
null,
self::ID,
array(),
$payment_source
$payment_source,
$wc_order
);
}

View file

@ -88,71 +88,37 @@ class ReturnUrlEndpoint {
WC()->session->set_customer_session_cookie( true );
}
// Check if we have a PayPal order in session (3DS return).
$order = $this->session_handler->order();
if ( $order ) {
try {
// Handle 3DS capture before processing.
$order = $this->handle_3ds_return( $order );
$wc_order_id = (int) $order->purchase_units()[0]->custom_id();
$wc_order = wc_get_order( $wc_order_id );
if ( ! $wc_order || ! is_a( $wc_order, \WC_Order::class ) ) {
wc_add_notice( __( 'Order information is missing. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
}
$payment_gateway = $this->get_payment_gateway( $wc_order->get_payment_method() );
if ( ! $payment_gateway ) {
wc_add_notice( __( 'Payment gateway is unavailable. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
}
$result = $payment_gateway->process_payment( $wc_order_id );
if ( isset( $result['result'] ) && $result['result'] === 'success' ) {
wp_safe_redirect( $result['redirect'] );
exit();
}
wc_add_notice( __( 'Payment processing failed. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
} catch ( Exception $e ) {
wc_add_notice( __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
}
}
// No order in session - handle regular PayPal returns.
// Check for token parameter - required for all returns.
if ( ! isset( $_GET['token'] ) ) {
wc_add_notice( __( 'Payment session expired. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
}
// Handle regular PayPal returns (non-3DS).
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$token = sanitize_text_field( wp_unslash( $_GET['token'] ) );
try {
$order = $this->order_endpoint->order( $token );
} catch ( Exception $exception ) {
$this->logger->warning( "Return URL endpoint failed to fetch order $token: " . $exception->getMessage() );
wc_add_notice( __( 'Could not retrieve payment information. Please try again.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
}
if ( $order->status()->is( OrderStatus::APPROVED ) || $order->status()->is( OrderStatus::COMPLETED ) ) {
$this->session_handler->replace_order( $order );
// Handle 3DS completion if needed.
if ( $this->needs_3ds_completion( $order ) ) {
try {
$order = $this->complete_3ds_verification( $order );
} catch ( Exception $e ) {
$this->logger->warning( "3DS completion failed for order $token: " . $e->getMessage() );
wc_add_notice( $this->get_3ds_error_message( $e ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
}
}
// Get WooCommerce order ID.
$wc_order_id = (int) $order->purchase_units()[0]->custom_id();
if ( ! $wc_order_id ) {
// We cannot finish processing here without WC order, but at least go into the continuation mode.
@ -192,7 +158,9 @@ class ReturnUrlEndpoint {
exit();
}
$this->logger->info( 'ReturnUrlEndpoint calling process_payment for gateway: ' . get_class( $payment_gateway ) );
$success = $payment_gateway->process_payment( $wc_order_id );
$this->logger->info( 'ReturnUrlEndpoint process_payment result: ' . wp_json_encode( $success ) );
if ( isset( $success['result'] ) && 'success' === $success['result'] ) {
add_filter(
@ -212,62 +180,70 @@ class ReturnUrlEndpoint {
exit();
}
/**
* Check if order needs 3DS completion.
*
* @param mixed $order The PayPal order.
* @return bool
*/
private function needs_3ds_completion( $order ): bool {
// If order is still CREATED after 3DS redirect, it needs to be captured.
return $order->status()->is( OrderStatus::CREATED );
}
/**
* Handle 3DS return and capture order if needed.
* Complete 3DS verification by capturing the order.
*
* @param mixed $order The PayPal order.
* @return mixed The processed order.
* @throws Exception When 3DS completion fails.
*/
private function handle_3ds_return( $order ) {
// If order is still CREATED after 3DS, it needs to be captured.
if ( $order->status()->is( OrderStatus::CREATED ) ) {
try {
// Capture the order.
$captured_order = $this->order_endpoint->capture( $order );
private function complete_3ds_verification( $order ) {
try {
// Capture the order.
$captured_order = $this->order_endpoint->capture( $order );
// Check if capture actually succeeded vs. payment declined.
if ( $captured_order->status()->is( OrderStatus::COMPLETED ) ) {
// Update session with captured order.
$this->session_handler->replace_order( $captured_order );
return $captured_order;
} else {
// Capture API succeeded but payment was declined.
throw new Exception( __( 'Payment was declined by the payment provider. Please try a different payment method.', 'woocommerce-paypal-payments' ) );
}
} catch ( DomainException $e ) {
// Handle 3DS authentication failures (Test Case 4: Unavailable).
// Clear session data since authentication failed.
$this->session_handler->destroy_session_data();
// Use native WooCommerce error handling.
wc_add_notice( __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
} catch ( RuntimeException $e ) {
if ( strpos( $e->getMessage(), 'declined' ) !== false ||
strpos( $e->getMessage(), 'PAYMENT_DENIED' ) !== false ||
strpos( $e->getMessage(), 'INSTRUMENT_DECLINED' ) !== false ||
strpos( $e->getMessage(), 'Payment provider declined' ) !== false ) {
$this->session_handler->destroy_session_data();
wc_add_notice( __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
}
throw $e;
} catch ( Exception $e ) {
$this->session_handler->destroy_session_data();
wc_add_notice( __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' );
wp_safe_redirect( wc_get_checkout_url() );
exit();
// Check if capture actually succeeded vs. payment declined.
if ( $captured_order->status()->is( OrderStatus::COMPLETED ) ) {
return $captured_order;
} else {
// Capture API succeeded but payment was declined.
throw new Exception( __( 'Payment was declined by the payment provider. Please try a different payment method.', 'woocommerce-paypal-payments' ) );
}
} catch ( DomainException $e ) {
// Handle 3DS authentication failures.
throw new Exception( __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ) );
} catch ( RuntimeException $e ) {
if ( strpos( $e->getMessage(), 'declined' ) !== false ||
strpos( $e->getMessage(), 'PAYMENT_DENIED' ) !== false ||
strpos( $e->getMessage(), 'INSTRUMENT_DECLINED' ) !== false ||
strpos( $e->getMessage(), 'Payment provider declined' ) !== false ) {
throw new Exception( __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ) );
}
throw $e;
}
}
/**
* Get user-friendly error message for 3DS failures.
*
* @param Exception $exception The exception.
* @return string
*/
private function get_3ds_error_message( Exception $exception ): string {
$error_message = $exception->getMessage();
if ( strpos( $error_message, '3D Secure' ) !== false ) {
return $error_message;
}
return $order;
if ( strpos( $error_message, 'declined' ) !== false ) {
return __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' );
}
return __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' );
}
/**
* Gets the appropriate payment gateway for the given payment method.
*