Commit 1515f3d7e6e for woocommerce

commit 1515f3d7e6ea4b2fba5dfdfc9ae7fa20c5219854
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Tue Aug 18 11:21:17 2026 +0200

    Fix Single Product and Add to Cart + Options redirects in subdirectories (#67402)

    * Fix Single Product and Add to Cart + Options redirects in subdirectories

    * Add changelog

    * wp_unslack()  variable

    * Create 'get_current_page_url()' util

    * Update plugins/woocommerce/src/Blocks/Utils/Utils.php

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

    * fix: preserve query string as is

    * test: add more test cases

    * Remove unnecessary PHPCS ignore

    * Add case in tests for use_trailing_slashes being set to false

    ---------

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
    Co-authored-by: Tung Du <dinhtungdu@gmail.com>

diff --git a/plugins/woocommerce/changelog/fix-67394-subdirectory-redirect b/plugins/woocommerce/changelog/fix-67394-subdirectory-redirect
new file mode 100644
index 00000000000..ed0a73057d9
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-67394-subdirectory-redirect
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Add to Cart + Options redirect when a plugin modifies the form and WooCommerce is installed in a subdirectory
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
index 32b65589c50..9fb37e03e24 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 namespace Automattic\WooCommerce\Blocks\BlockTypes;

 use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
+use Automattic\WooCommerce\Blocks\Utils\Utils as BlocksUtils;
 use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils;
 use Automattic\WooCommerce\Enums\ProductType;

@@ -294,7 +295,6 @@ class AddToCartForm extends AbstractBlock {
 	 * @return string The current URL.
 	 */
 	public function add_to_cart_form_action() {
-		global $wp;
-		return home_url( add_query_arg( $_GET, $wp->request ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		return BlocksUtils::get_current_page_url();
 	}
 }
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
index 383cd907f1a..17c65282742 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
@@ -7,6 +7,7 @@ use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
 use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
 use Automattic\WooCommerce\Blocks\Package;
 use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
+use Automattic\WooCommerce\Blocks\Utils\Utils as BlocksUtils;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
 use Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController;
@@ -553,7 +554,7 @@ class AddToCartWithOptions extends AbstractBlock {
 			$form_attributes         = '';
 			$legacy_mode             = 'yes' === $cart_redirect_after_add || $this->has_form_elements( $hooks_before ) || $this->has_form_elements( $hooks_after );
 			if ( $legacy_mode ) {
-				$action_url = home_url( add_query_arg( null, null ) );
+				$action_url = BlocksUtils::get_current_page_url();

 				// If an extension is hooking into the form or we need to redirect to the cart,
 				// we fall back to a regular HTML form.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php b/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
index ccdb0e872ed..8cb78ff6c85 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
@@ -2,6 +2,7 @@
 namespace Automattic\WooCommerce\Blocks\BlockTypes;

 use Automattic\WooCommerce\Blocks\Utils\ProductDataUtils;
+use Automattic\WooCommerce\Blocks\Utils\Utils as BlocksUtils;
 use Automattic\WooCommerce\Enums\ProductType;

 /**
@@ -196,7 +197,7 @@ class SingleProduct extends AbstractBlock {
 		if ( post_password_required( $product_id ) ) {
 			$password_form = get_the_password_form( $product_id );
 			$html          = new \WP_HTML_Tag_Processor( $password_form );
-			$current_url   = home_url( add_query_arg( null, null ) );
+			$current_url   = BlocksUtils::get_current_page_url();

 			while ( $html->next_tag( array( 'tag_name' => 'input' ) ) ) {
 				if ( 'redirect_to' !== $html->get_attribute( 'name' ) ) {
diff --git a/plugins/woocommerce/src/Blocks/Utils/Utils.php b/plugins/woocommerce/src/Blocks/Utils/Utils.php
index 90cdb14adb4..e4ead6c62bb 100644
--- a/plugins/woocommerce/src/Blocks/Utils/Utils.php
+++ b/plugins/woocommerce/src/Blocks/Utils/Utils.php
@@ -49,4 +49,24 @@ class Utils {
 		}
 		return $src;
 	}
+
+	/**
+	 * Get the current page URL using the request path relative to home.
+	 *
+	 * @since 11.1.0
+	 * @return string The current page URL.
+	 */
+	public static function get_current_page_url() {
+		global $wp;
+
+		$request_path = is_object( $wp ) && isset( $wp->request ) && is_string( $wp->request ) ? $wp->request : '';
+		$url          = home_url( user_trailingslashit( $request_path ) );
+
+		if ( isset( $_SERVER['QUERY_STRING'] ) && is_string( $_SERVER['QUERY_STRING'] ) && '' !== $_SERVER['QUERY_STRING'] ) {
+			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserving the raw query string encoding and delimiters.
+			$url .= '?' . wp_unslash( $_SERVER['QUERY_STRING'] );
+		}
+
+		return $url;
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Utils/UtilsTest.php b/plugins/woocommerce/tests/php/src/Blocks/Utils/UtilsTest.php
index 369cc8b55b5..e555753f154 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Utils/UtilsTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Utils/UtilsTest.php
@@ -150,4 +150,96 @@ class UtilsTest extends WC_Unit_Test_Case {
 			array( '/wp-content', '/wp-includes/x.js', false ),
 		);
 	}
+
+	/**
+	 * Get the current page URL after temporarily setting request state.
+	 *
+	 * @param string      $request_path         The request path relative to home.
+	 * @param string|null $query_string         The raw query string, or null to omit it.
+	 * @param bool        $use_trailing_slashes Whether permalinks should use trailing slashes.
+	 * @return string
+	 */
+	private function get_current_page_url_with_request_state( string $request_path, ?string $query_string, bool $use_trailing_slashes ): string {
+		global $wp, $wp_rewrite;
+
+		$original_request              = $wp->request;
+		$original_query_string_exists  = array_key_exists( 'QUERY_STRING', $_SERVER ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+		$original_query_string         = $_SERVER['QUERY_STRING'] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+		$original_use_trailing_slashes = $wp_rewrite->use_trailing_slashes;
+
+		try {
+			$wp->request                      = $request_path;
+			$wp_rewrite->use_trailing_slashes = $use_trailing_slashes;
+
+			if ( null === $query_string ) {
+				unset( $_SERVER['QUERY_STRING'] );
+			} else {
+				$_SERVER['QUERY_STRING'] = $query_string;
+			}
+
+			return Utils::get_current_page_url();
+		} finally {
+			$wp->request                      = $original_request;
+			$wp_rewrite->use_trailing_slashes = $original_use_trailing_slashes;
+
+			if ( $original_query_string_exists ) {
+				$_SERVER['QUERY_STRING'] = $original_query_string;
+			} else {
+				unset( $_SERVER['QUERY_STRING'] );
+			}
+		}
+	}
+
+	/**
+	 * @testdox get_current_page_url() preserves request URL components exactly.
+	 * @dataProvider provider_current_page_url_cases
+	 *
+	 * @param string      $request_path         The request path relative to home.
+	 * @param string|null $query_string         The raw query string, or null to omit it.
+	 * @param string      $expected_path        The expected path relative to home.
+	 * @param bool        $use_trailing_slashes Whether permalinks should use trailing slashes.
+	 */
+	public function test_get_current_page_url( string $request_path, ?string $query_string, string $expected_path, bool $use_trailing_slashes ): void {
+		$url = $this->get_current_page_url_with_request_state( $request_path, $query_string, $use_trailing_slashes );
+
+		$this->assertSame(
+			untrailingslashit( home_url() ) . $expected_path,
+			$url,
+			'The current page URL should preserve all request components exactly.'
+		);
+	}
+
+	/**
+	 * Current page URL inputs and their exact expected paths.
+	 *
+	 * @return array<string, array{string, string|null, string, bool}>
+	 */
+	public function provider_current_page_url_cases(): array {
+		return array(
+			'encoded label query'                   => array(
+				'product/hoodie',
+				'label=Black%20%26%20White',
+				'/product/hoodie/?label=Black%20%26%20White',
+				true,
+			),
+			'question mark query value'             => array(
+				'search-results',
+				'search=?',
+				'/search-results/?search=?',
+				true,
+			),
+			'lone zero query string'                => array(
+				'product/hoodie',
+				'0',
+				'/product/hoodie/?0',
+				true,
+			),
+			'encoded label query no trailing slash' => array(
+				'product/hoodie',
+				'label=Black%20%26%20White',
+				'/product/hoodie?label=Black%20%26%20White',
+				false,
+			),
+		);
+	}
 }