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,
+ ),
+ );
+ }
}