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