Commit 851842f97a for woocommerce
commit 851842f97a5e00e59705f19e52a8a0a254b50548
Author: Mayisha <33387139+Mayisha@users.noreply.github.com>
Date: Mon Nov 24 11:09:49 2025 +0600
PayPal Standard: Normalize the URL in create order request data to ensure it is absolute (#62011)
* normalize the URL to ensure it is absolute
* replace encoded ampersand & with actual ampersand &
* move esc_url_raw inside normalize function
* add tests
diff --git a/plugins/woocommerce/changelog/62011-fix-invalid-url b/plugins/woocommerce/changelog/62011-fix-invalid-url
new file mode 100644
index 0000000000..de0f366618
--- /dev/null
+++ b/plugins/woocommerce/changelog/62011-fix-invalid-url
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Normalize the URLs in PayPal Standard request data to prevent errors due to unsupported URL format in PayPal.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php
index f50db5f409..9453bea803 100644
--- a/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php
+++ b/plugins/woocommerce/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php
@@ -454,9 +454,9 @@ class WC_Gateway_Paypal_Request {
'user_action' => WC_Gateway_Paypal_Constants::USER_ACTION_PAY_NOW,
'shipping_preference' => $shipping_preference,
// Customer redirected here on approval.
- 'return_url' => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ),
+ 'return_url' => $this->normalize_url_for_paypal( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ),
// Customer redirected here on cancellation.
- 'cancel_url' => esc_url_raw( $order->get_cancel_order_url_raw() ),
+ 'cancel_url' => $this->normalize_url_for_paypal( $order->get_cancel_order_url_raw() ),
// Convert WordPress locale format (e.g., 'en_US') to PayPal's expected format (e.g., 'en-US').
'locale' => str_replace( '_', '-', $src_locale ),
'app_switch_preference' => array(
@@ -488,7 +488,7 @@ class WC_Gateway_Paypal_Request {
) ) {
$params['payment_source'][ $payment_source ]['experience_context']['order_update_callback_config'] = array(
'callback_events' => array( 'SHIPPING_ADDRESS', 'SHIPPING_OPTIONS' ),
- 'callback_url' => esc_url_raw( rest_url( 'wc/v3/paypal-standard/update-shipping' ) ),
+ 'callback_url' => $this->normalize_url_for_paypal( rest_url( 'wc/v3/paypal-standard/update-shipping' ) ),
);
}
@@ -513,7 +513,7 @@ class WC_Gateway_Paypal_Request {
),
$request_origin
);
- $params['payment_source'][ $payment_source ]['experience_context']['cancel_url'] = esc_url_raw( $cancel_url );
+ $params['payment_source'][ $payment_source ]['experience_context']['cancel_url'] = $this->normalize_url_for_paypal( $cancel_url );
}
}
@@ -781,6 +781,39 @@ class WC_Gateway_Paypal_Request {
return $alpha2;
}
+ /**
+ * Normalize a URL for PayPal. PayPal requires absolute URLs with protocol.
+ *
+ * @param string $url The URL to check.
+ * @return string Normalized URL.
+ */
+ private function normalize_url_for_paypal( $url ) {
+ // Replace encoded ampersand with actual ampersand.
+ // In some cases, the URL may contain encoded ampersand but PayPal expects the actual ampersand.
+ // PayPal request fails if the URL contains encoded ampersand.
+ $url = str_replace( '&', '&', $url );
+
+ // If the URL is already the home URL, return it.
+ if ( strpos( $url, home_url() ) === 0 ) {
+ return esc_url_raw( $url );
+ }
+
+ // Return the URL if it is already absolute (contains ://).
+ if ( strpos( $url, '://' ) !== false ) {
+ return esc_url_raw( $url );
+ }
+
+ $home_url = untrailingslashit( home_url() );
+
+ // If the URL is relative (starts with /), prepend the home URL.
+ if ( strpos( $url, '/' ) === 0 ) {
+ return esc_url_raw( $home_url . $url );
+ }
+
+ // Prepend home URL with a slash.
+ return esc_url_raw( $home_url . '/' . $url );
+ }
+
/**
* Fetch the PayPal client-id from the Transact platform.
*
diff --git a/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php b/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php
index 1d0f3b25d1..23f970ed8a 100644
--- a/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php
+++ b/plugins/woocommerce/tests/php/includes/gateways/paypal/class-wc-gateway-paypal-request-test.php
@@ -199,4 +199,92 @@ class WC_Gateway_Paypal_Request_Test extends \WC_Unit_Test_Case {
public function return_blog_token( $value ) {
return array( 'blog_token' => 'IAM.AJETPACKBLOGTOKEN' );
}
+
+ /**
+ * Data provider for normalize_url_for_paypal test scenarios.
+ *
+ * @return array
+ */
+ public function provider_normalize_url_scenarios() {
+ return array(
+ 'absolute_url_https' => array(
+ 'input' => 'https://example.com/checkout',
+ 'expected' => 'https://example.com/checkout',
+ ),
+ 'absolute_url_http' => array(
+ 'input' => 'http://example.com/checkout',
+ 'expected' => 'http://example.com/checkout',
+ ),
+ 'relative_url_with_leading_slash' => array(
+ 'input' => '/checkout',
+ 'expected' => home_url() . '/checkout',
+ ),
+ 'relative_url_without_leading_slash' => array(
+ 'input' => 'checkout',
+ 'expected' => home_url() . '/checkout',
+ ),
+ 'url_with_encoded_ampersand' => array(
+ 'input' => 'https://example.com/checkout?foo=bar&baz=qux',
+ 'expected' => 'https://example.com/checkout?foo=bar&baz=qux',
+ ),
+ 'url_with_multiple_encoded_ampersands' => array(
+ 'input' => 'https://example.com/checkout?a=1&b=2&c=3',
+ 'expected' => 'https://example.com/checkout?a=1&b=2&c=3',
+ ),
+ 'relative_url_with_encoded_ampersand' => array(
+ 'input' => '/checkout?foo=bar&baz=qux',
+ 'expected' => home_url() . '/checkout?foo=bar&baz=qux',
+ ),
+ 'url_starting_with_home_url' => array(
+ 'input' => home_url() . '/checkout',
+ 'expected' => home_url() . '/checkout',
+ ),
+ 'url_starting_with_home_url_and_encoded_ampersand' => array(
+ 'input' => home_url() . '/checkout?foo=bar&baz=qux',
+ 'expected' => home_url() . '/checkout?foo=bar&baz=qux',
+ ),
+ 'empty_string' => array(
+ 'input' => '',
+ 'expected' => home_url() . '/',
+ ),
+ 'url_with_query_params' => array(
+ 'input' => '/checkout?order_id=123&key=abc',
+ 'expected' => home_url() . '/checkout?order_id=123&key=abc',
+ ),
+ 'url_with_fragment' => array(
+ 'input' => '/checkout#payment',
+ 'expected' => home_url() . '/checkout#payment',
+ ),
+ 'url_with_different_domain' => array(
+ 'input' => 'https://external.com/callback',
+ 'expected' => 'https://external.com/callback',
+ ),
+ 'url_with_html_entities' => array(
+ 'input' => '/checkout?product=Test<Product>',
+ 'expected' => home_url() . '/checkout?product=TestProduct',
+ ),
+ );
+ }
+
+ /**
+ * Test normalize_url_for_paypal with various URL scenarios.
+ *
+ * @dataProvider provider_normalize_url_scenarios
+ *
+ * @param string $input The input URL to normalize.
+ * @param string $expected The expected normalized URL.
+ */
+ public function test_normalize_url_for_paypal( $input, $expected ) {
+ $gateway = new WC_Gateway_Paypal();
+ $request = new WC_Gateway_Paypal_Request( $gateway );
+
+ // Use reflection to access the private method.
+ $reflection = new ReflectionClass( $request );
+ $method = $reflection->getMethod( 'normalize_url_for_paypal' );
+ $method->setAccessible( true );
+
+ $result = $method->invokeArgs( $request, array( $input ) );
+
+ $this->assertEquals( $expected, $result );
+ }
}