Commit ff0da58ba2 for woocommerce
commit ff0da58ba2315671b5b20a9380f9ba9b88c4e25f
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date: Thu Feb 12 21:14:50 2026 +0800
Fix PHP warnings when product categories have non-sequential array keys (#63263)
* Fix PHP warnings when product categories have non-sequential array keys
Plugins filtering get_the_terms may remove elements without re-indexing
the array, causing $terms[0] to fail with "Undefined array key 0".
Use array_values() to ensure sequential keys before accessing the array.
* Add changefile(s) from automation for the following project(s): woocommerce
* Update plugins/woocommerce/includes/wc-product-functions.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* test: improve test isolation with try/finally cleanup
Address PR feedback by ensuring proper test isolation:
- Save and restore woocommerce_permalinks option
- Use try/finally to guarantee filter removal and cleanup
- Narrow filter scope by checking post_id
* Update plugins/woocommerce/includes/wc-product-functions.php
Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
diff --git a/plugins/woocommerce/changelog/63263-fix-wooplug-6280-non-sequential-term-keys b/plugins/woocommerce/changelog/63263-fix-wooplug-6280-non-sequential-term-keys
new file mode 100644
index 0000000000..41dcdec926
--- /dev/null
+++ b/plugins/woocommerce/changelog/63263-fix-wooplug-6280-non-sequential-term-keys
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix PHP warnings when product categories have non-sequential array keys due to plugin filters.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index 3677172030..fa7695f6f7 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -276,7 +276,10 @@ function wc_product_post_type_link( $permalink, $post ) {
// Get the custom taxonomy terms in use by this post.
$terms = get_the_terms( $post->ID, 'product_cat' );
- if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
+ if ( ! empty( $terms ) && ! is_wp_error( $terms ) && is_array( $terms ) ) {
+ // Re-index array to ensure sequential keys starting from 0 since filters may remove some keys.
+ $terms = array_values( $terms );
+
// Find the deepest category (most ancestors) for the permalink.
$deepest_term = $terms[0];
$deepest_ancestors = $deepest_term->parent ? get_ancestors( $deepest_term->term_id, 'product_cat' ) : array();
diff --git a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
index 50b2632103..d7d60b9ff7 100644
--- a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
@@ -591,4 +591,63 @@ class WC_Product_Functions_Tests extends \WC_Unit_Test_Case {
WC_Helper_Product::delete_product( $product->get_id() );
wp_delete_term( $category_term['term_id'], 'product_cat' );
}
+
+ /**
+ * @testdox Product permalink handles non-sequential array keys from get_the_terms filters.
+ */
+ public function test_wc_product_post_type_link_handles_non_sequential_term_array_keys() {
+ // Create categories.
+ $category1_term = wp_insert_term( 'Category One', 'product_cat' );
+ $category2_term = wp_insert_term( 'Category Two', 'product_cat' );
+
+ $product = WC_Helper_Product::create_simple_product();
+ wp_set_object_terms(
+ $product->get_id(),
+ array( $category1_term['term_id'], $category2_term['term_id'] ),
+ 'product_cat'
+ );
+
+ $original_permalinks = get_option( 'woocommerce_permalinks' );
+ $filter_callback = null;
+
+ try {
+ update_option( 'woocommerce_permalinks', array( 'product_base' => '/shop/%product_cat%' ) );
+ $product_post = get_post( $product->get_id() );
+
+ // Simulate a plugin filter that removes a term without re-indexing the array.
+ // This creates non-sequential keys (e.g., key 0 is removed, leaving only key 1).
+ $filter_callback = function ( $terms, $post_id, $taxonomy ) use ( $category1_term, $product ) {
+ if ( 'product_cat' !== $taxonomy || ! is_array( $terms ) || $post_id !== $product->get_id() ) {
+ return $terms;
+ }
+ foreach ( $terms as $key => $term ) {
+ if ( $term->term_id === $category1_term['term_id'] ) {
+ unset( $terms[ $key ] ); // Intentionally don't re-index.
+ break;
+ }
+ }
+ return $terms; // Returns array with non-sequential keys.
+ };
+ add_filter( 'get_the_terms', $filter_callback, 10, 3 );
+
+ // This should not trigger PHP warnings about undefined array key 0.
+ $permalink = wc_product_post_type_link( '/shop/%product_cat%/' . $product_post->post_name . '/', $product_post );
+
+ // Should use the remaining category (Category Two).
+ $category2_slug = get_term( $category2_term['term_id'], 'product_cat' )->slug;
+ $this->assertStringContainsString(
+ '/' . $category2_slug . '/',
+ $permalink,
+ 'Permalink should contain the remaining category slug after filter removes one'
+ );
+ } finally {
+ if ( null !== $filter_callback ) {
+ remove_filter( 'get_the_terms', $filter_callback, 10 );
+ }
+ update_option( 'woocommerce_permalinks', $original_permalinks );
+ WC_Helper_Product::delete_product( $product->get_id() );
+ wp_delete_term( $category2_term['term_id'], 'product_cat' );
+ wp_delete_term( $category1_term['term_id'], 'product_cat' );
+ }
+ }
}