Commit 25b4704286f for woocommerce
commit 25b4704286ffdbe5a4b46693f3b142fd9dcafc1a
Author: Luigi Teschio <gigitux@gmail.com>
Date: Wed Oct 7 17:26:11 2026 +0200
Prevent product attribute rendering from crashing on missing terms (#69543)
* Prevent product attribute rendering from crashing on missing terms
* Add changelog entry for missing product attribute links
* improve test
* fix get_term_link in ProductSpecifications block
diff --git a/plugins/woocommerce/changelog/fix-66478-product-attribute-term-link b/plugins/woocommerce/changelog/fix-66478-product-attribute-term-link
new file mode 100644
index 00000000000..949a25ff0a8
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-66478-product-attribute-term-link
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent product pages from crashing when an attribute archive link is unavailable.
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index 677d99a5b36..f355cdc618d 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -4292,7 +4292,8 @@ function wc_display_product_attributes( $product ) {
$value_name = esc_html( $attribute_value->name );
if ( $attribute_taxonomy && $attribute_taxonomy->attribute_public ) {
- $values[] = '<a href="' . esc_url( get_term_link( $attribute_value->term_id, $attribute->get_name() ) ) . '" rel="tag">' . $value_name . '</a>';
+ $term_link = get_term_link( $attribute_value->term_id, $attribute->get_name() );
+ $values[] = is_wp_error( $term_link ) ? $value_name : '<a href="' . esc_url( $term_link ) . '" rel="tag">' . $value_name . '</a>';
} else {
$values[] = $value_name;
}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index c0c3b363487..a11a156932d 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -35118,7 +35118,7 @@ parameters:
-
message: '#^Parameter \#1 \$url of function esc_url expects string, string\|WP_Error given\.$#'
identifier: argument.type
- count: 2
+ count: 1
path: includes/wc-template-functions.php
-
@@ -51711,12 +51711,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/ProductSpecifications.php
- -
- message: '#^Parameter \#1 \$url of function esc_url expects string, string\|WP_Error given\.$#'
- identifier: argument.type
- count: 1
- path: src/Blocks/BlockTypes/ProductSpecifications.php
-
-
message: '#^Parameter \#1 \$weight of function wc_format_weight expects float, string given\.$#'
identifier: argument.type
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductSpecifications.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductSpecifications.php
index 165130532e3..0fbc73514c1 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductSpecifications.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductSpecifications.php
@@ -85,7 +85,8 @@ class ProductSpecifications extends AbstractBlock {
$value_name = esc_html( $attribute_value->name );
if ( $attribute_taxonomy && $attribute_taxonomy->attribute_public ) {
- $values[] = '<a href="' . esc_url( get_term_link( $attribute_value->term_id, $attribute->get_name() ) ) . '" rel="tag">' . $value_name . '</a>';
+ $term_link = get_term_link( $attribute_value->term_id, $attribute->get_name() );
+ $values[] = is_wp_error( $term_link ) ? $value_name : '<a href="' . esc_url( $term_link ) . '" rel="tag">' . $value_name . '</a>';
} else {
$values[] = $value_name;
}
diff --git a/plugins/woocommerce/tests/php/includes/wc-template-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-template-functions-test.php
index c7d8c8720ab..e673bcaae89 100644
--- a/plugins/woocommerce/tests/php/includes/wc-template-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-template-functions-test.php
@@ -358,6 +358,38 @@ class WC_Template_Functions_Tests extends \WC_Unit_Test_Case {
$this->assertStringContainsString( 'Matte', $markup );
}
+ /**
+ * @testdox Displaying product attributes renders plain text when a term link cannot be resolved.
+ */
+ public function test_display_product_attributes_does_not_crash_for_deleted_term(): void {
+ global $wc_product_attributes;
+
+ $previous_attributes = $wc_product_attributes;
+ $attribute = WC_Helper_Product::create_product_attribute_object( 'Archive Finish', array( 'Matte & Gloss' ) );
+ $taxonomy = $attribute->get_name();
+
+ try {
+ $wc_product_attributes[ $taxonomy ]->attribute_public = true;
+
+ $product = new WC_Product_Simple();
+ $product->set_attributes( array( $attribute ) );
+
+ $term = get_term( $attribute->get_options()[0], $taxonomy );
+ wp_delete_term( $term->term_id, $taxonomy );
+
+ add_filter( 'woocommerce_get_product_terms', static fn() => array( $term ) );
+ ob_start();
+ wc_display_product_attributes( $product );
+ $markup = (string) ob_get_clean();
+ } finally {
+ unregister_taxonomy( $taxonomy );
+ $wc_product_attributes = $previous_attributes;
+ }
+
+ $this->assertStringContainsString( 'Matte & Gloss', $markup, 'The term name should be rendered as escaped text.' );
+ $this->assertStringNotContainsString( '<a ', $markup, 'The deleted term should not be linked.' );
+ }
+
/**
* @testdox The empty cart message should be wrapped in the shared notices wrapper.
*/
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductSpecificationsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductSpecificationsTest.php
new file mode 100644
index 00000000000..a5717df5dc0
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductSpecificationsTest.php
@@ -0,0 +1,58 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+use WC_Product_Simple;
+use WC_Unit_Test_Case;
+use WP_Block;
+
+/**
+ * Tests for the Product Specifications block type.
+ */
+class ProductSpecificationsTest extends WC_Unit_Test_Case {
+
+ /**
+ * @testdox Product specifications render plain text when a term link cannot be resolved.
+ */
+ public function test_render_does_not_crash_for_deleted_term(): void {
+ global $wc_product_attributes;
+
+ $previous_attributes = $wc_product_attributes;
+ $attribute = WC_Helper_Product::create_product_attribute_object( 'Archive Finish', array( 'Matte & Gloss' ) );
+ $taxonomy = $attribute->get_name();
+
+ try {
+ $wc_product_attributes[ $taxonomy ]->attribute_public = true;
+
+ $product = new WC_Product_Simple();
+ $product->set_attributes( array( $attribute ) );
+ $product->save();
+
+ $term = get_term( $attribute->get_options()[0], $taxonomy );
+ wp_delete_term( $term->term_id, $taxonomy );
+
+ add_filter( 'woocommerce_get_product_terms', static fn() => array( $term ) );
+
+ $sut = new WP_Block(
+ array(
+ 'blockName' => 'woocommerce/product-specifications',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => '',
+ 'innerContent' => array(),
+ ),
+ array( 'postId' => $product->get_id() )
+ );
+
+ $markup = $sut->render();
+ } finally {
+ unregister_taxonomy( $taxonomy );
+ $wc_product_attributes = $previous_attributes;
+ }
+
+ $this->assertStringContainsString( 'Matte & Gloss', $markup, 'The term name should be rendered as escaped text.' );
+ $this->assertStringNotContainsString( '<a ', $markup, 'The deleted term should not be linked.' );
+ }
+}