Commit 39a33686334 for woocommerce

commit 39a33686334fc32beb0554534cf9172a0e0bc5e7
Author: Brian <brian@brianhaas.li>
Date:   Sun May 3 01:17:14 2026 +0200

    Fix: Array to string conversion warning in wc_query_string_form_fields() with nested query params (#64010)

    * fix Array to string conversion php8 message

    * Add changefile(s) from automation for the following project(s): woocommerce

    * fix placeholders leak

    * add tests

    * add prettyboymp suggestion

    Co-authored-by: Michael Pretty <prettyboymp@users.noreply.github.com>

    * remove part from phpstan baseline

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Michael Pretty <prettyboymp@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/64010-30819-php8-warning b/plugins/woocommerce/changelog/64010-30819-php8-warning
new file mode 100644
index 00000000000..35a676e0cb5
--- /dev/null
+++ b/plugins/woocommerce/changelog/64010-30819-php8-warning
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix array to string conversion warning in wc_query_string_form_fields() when URL contains nested query string parameters.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index dcbdeeb72d8..4b20e650060 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -807,12 +807,24 @@ function wc_query_string_form_fields( $values = null, $exclude = array(), $curre
 			// Parse the string.
 			parse_str( $query_string, $parsed_query_string );

-			// Convert the full-stops, pluses and spaces back and add to values array.
-			foreach ( $parsed_query_string as $key => $value ) {
-				$new_key            = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $key );
-				$new_value          = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $value );
-				$values[ $new_key ] = $new_value;
-			}
+			// Convert the full-stops, pluses and spaces back in all scalar values (any depth).
+			array_walk_recursive(
+				$parsed_query_string,
+				function ( &$value ) use ( $replace_chars ) {
+					$value = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $value );
+				}
+			);
+
+			// Restore placeholders in keys at every depth, then add to values array.
+			$restore_keys = function ( $items ) use ( &$restore_keys, $replace_chars ) {
+				$out = array();
+				foreach ( $items as $key => $value ) {
+					$key         = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $key );
+					$out[ $key ] = is_array( $value ) ? $restore_keys( $value ) : $value;
+				}
+				return $out;
+			};
+			$values       = $restore_keys( $parsed_query_string );
 		}
 	}
 	$html = '';
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 18433e313e0..cd38064367e 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -36363,12 +36363,6 @@ parameters:
 			count: 2
 			path: includes/wc-template-functions.php

-		-
-			message: '#^Parameter \#3 \$subject of function str_replace expects array\<string\>\|string, int\|string given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/wc-template-functions.php
-
 		-
 			message: '#^Property WooCommerce\:\:\$cart \(WC_Cart\) in isset\(\) is not nullable\.$#'
 			identifier: isset.property
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php
index b41e3d3b9c4..88be12c0e4f 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php
@@ -217,6 +217,45 @@ class WC_Tests_Template_Functions extends WC_Unit_Test_Case {
 		$this->assertEquals( $expected_html, $actual_html );
 	}

+	/**
+	 * Test wc_query_string_form_fields with nested array params.
+	 *
+	 * @dataProvider provide_nested_array_cases
+	 *
+	 * @param string $url            URL to parse.
+	 * @param string $expected_name  Expected hidden field name attribute.
+	 * @param string $expected_value Expected hidden field value attribute.
+	 * @return void
+	 */
+	public function test_wc_query_string_form_fields_nested_arrays( string $url, string $expected_name, string $expected_value ): void {
+		$html = wc_query_string_form_fields( $url, array(), '', true );
+
+		$this->assertStringContainsString( 'name="' . $expected_name . '"', $html );
+		$this->assertStringContainsString( 'value="' . $expected_value . '"', $html );
+		$this->assertStringNotContainsString( '{dot}', $html );
+		$this->assertStringNotContainsString( '{plus}', $html );
+	}
+
+	/**
+	 * Data provider for test_wc_query_string_form_fields_nested_arrays.
+	 *
+	 * @return array[]
+	 */
+	public function provide_nested_array_cases(): array {
+		return array(
+			// Baseline: nested params without any special chars.
+			'nested baseline'      => array( 'https://x/?products[1][id]=12345', 'products[1][id]', '12345' ),
+			// Nested params with dots in nested keys.
+			'dot in nested key'    => array( 'https://x/?products[1.5][id]=12345', 'products[1.5][id]', '12345' ),
+			// Nested params with dots in nested values.
+			'dot in nested value'  => array( 'https://x/?products[1][price]=12.50', 'products[1][price]', '12.50' ),
+			// Same as dot-in-key case but with + instead of .
+			'plus in nested key'   => array( 'https://x/?products[a+b][id]=12345', 'products[a+b][id]', '12345' ),
+			// Same as dot-in-value case but with + instead of .
+			'plus in nested value' => array( 'https://x/?products[1][label]=hello+world', 'products[1][label]', 'hello+world' ),
+		);
+	}
+
 	/**
 	 * Test test_wc_get_pay_buttons().
 	 */