Commit 70480c92597 for woocommerce
commit 70480c92597757c7efb8d29473ed5714166a3df0
Author: Lucio Giannotta <lucio.giannotta@a8c.com>
Date: Tue Sep 22 15:39:45 2026 +0200
Prevent recursive variation gallery rendering (#68939)
* Prevent recursive variation gallery rendering
* Add changelog entry for variation gallery recursion fix
diff --git a/plugins/woocommerce/changelog/wooplug-7677-variation-gallery-recursion b/plugins/woocommerce/changelog/wooplug-7677-variation-gallery-recursion
new file mode 100644
index 00000000000..b346e4ee7c0
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-7677-variation-gallery-recursion
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent variation gallery rendering from recursively requesting variation data.
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index a7ae30f526a..45ae9bfe353 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -1965,19 +1965,29 @@ function wc_render_product_image_template_for( WC_Product $product ): string {
*
* @param WC_Product $product Product being rendered.
* @param mixed $image_ids Image IDs to substitute. Will be normalized.
- * @return string
+ * @return string Rendered gallery HTML, or an empty string for a re-entrant render of the same product.
*/
function wc_render_product_image_template_for_image_ids( WC_Product $product, $image_ids ): string {
+ static $rendering_product_galleries = array();
+
+ $product_id = $product->get_id();
+ if ( isset( $rendering_product_galleries[ $product_id ] ) ) {
+ return '';
+ }
+
$normalized = array_values( array_unique( array_map( 'intval', array_filter( (array) $image_ids ) ) ) );
$featured_id = $normalized[0] ?? 0;
$gallery_ids = array_slice( $normalized, 1 );
$remove_overrides = wc_apply_product_image_overrides( $product, $featured_id, $gallery_ids );
+ $rendering_product_galleries[ $product_id ] = true;
+
try {
return wc_render_product_image_template_for( $product );
} finally {
$remove_overrides();
+ unset( $rendering_product_galleries[ $product_id ] );
}
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
index f7f051c7f68..e4e45929659 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-product-variable-test.php
@@ -232,6 +232,82 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
$this->assertNotEmpty( $available_variation['gallery_images_html'] );
}
+ /**
+ * @testdox The product image template does not render variation galleries recursively when a gallery callback requests variation data.
+ */
+ public function test_product_image_template_does_not_render_variation_galleries_recursively(): void {
+ global $product;
+
+ list( $test_product ) = $this->create_variation_gallery_fixture();
+ $previous_product = $product;
+ $product = $test_product;
+ $hook_calls = 0;
+ $callback_results = array();
+ $buffer_level = ob_get_level();
+ $callback = static function () use ( &$hook_calls, &$callback_results ) {
+ global $product;
+
+ if ( ! $product instanceof WC_Product_Variable ) {
+ throw new RuntimeException( 'Expected the gallery template to expose the variable product.' );
+ }
+
+ ++$hook_calls;
+ if ( 2 < $hook_calls ) {
+ throw new RuntimeException( 'The gallery template was rendered recursively.' );
+ }
+
+ $callback_results[] = $product->get_available_variations();
+ };
+ add_action( 'woocommerce_product_thumbnails', $callback );
+
+ ob_start();
+ try {
+ woocommerce_show_product_images();
+ $markup = (string) ob_get_clean();
+ } finally {
+ while ( ob_get_level() > $buffer_level ) {
+ ob_end_clean();
+ }
+ remove_action( 'woocommerce_product_thumbnails', $callback );
+ $product = $previous_product;
+ }
+
+ $this->assertSame( 2, $hook_calls, 'The callback should run for the product template and its first variation gallery render.' );
+ $this->assertCount( 2, $callback_results, 'Both bounded callback invocations should return variation data.' );
+ $this->assertSame( '', $callback_results[0][0]['gallery_images_html'], 'Re-entrant variation data should omit gallery markup.' );
+ $this->assertNotEmpty( $callback_results[1][0]['gallery_images_html'], 'The first variation data request should retain gallery markup.' );
+ $this->assertNotEmpty( $markup, 'The product image template should still render.' );
+ }
+
+ /**
+ * @testdox 'get_available_variations' allows a gallery callback to render a different product gallery.
+ */
+ public function test_get_available_variations_allows_nested_gallery_for_different_product(): void {
+ list( $outer_product ) = $this->create_variation_gallery_fixture();
+ list( $nested_product ) = $this->create_variation_gallery_fixture();
+ $rendered_product_ids = array();
+ $nested_result = null;
+ $callback = static function () use ( $outer_product, $nested_product, &$rendered_product_ids, &$nested_result ) {
+ global $product;
+
+ $rendered_product_ids[] = $product->get_id();
+ if ( $outer_product->get_id() === $product->get_id() ) {
+ $nested_result = $nested_product->get_available_variations();
+ }
+ };
+ add_action( 'woocommerce_product_thumbnails', $callback );
+
+ try {
+ $outer_product->get_available_variations();
+ } finally {
+ remove_action( 'woocommerce_product_thumbnails', $callback );
+ }
+
+ $this->assertContains( $outer_product->get_id(), $rendered_product_ids, 'The outer product gallery should render.' );
+ $this->assertContains( $nested_product->get_id(), $rendered_product_ids, 'The nested product gallery should render.' );
+ $this->assertNotEmpty( $nested_result[0]['gallery_images_html'], 'A different product should retain its gallery markup.' );
+ }
+
/**
* @testdox 'get_available_variation' falls back to the variation's own gallery when the variation featured image is stale.
*/