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 &#038 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( '&#038;', '&', $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&#038;baz=qux',
+				'expected' => 'https://example.com/checkout?foo=bar&baz=qux',
+			),
+			'url_with_multiple_encoded_ampersands' => array(
+				'input'    => 'https://example.com/checkout?a=1&#038;b=2&#038;c=3',
+				'expected' => 'https://example.com/checkout?a=1&b=2&c=3',
+			),
+			'relative_url_with_encoded_ampersand'  => array(
+				'input'    => '/checkout?foo=bar&#038;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&#038;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 );
+	}
 }