Commit 21628fe7fbb for woocommerce

commit 21628fe7fbbd82fc7852325088e052115e80d53e
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Tue Oct 13 12:31:30 2026 +0200

    Fix legacy Extensions redirect fatal when categories fail to load (#69599)

    * Fix legacy Extensions redirect fatal when categories fail to load

    WC_Admin_Addons::get_sections() returns false when the WooCommerce.com
    categories request fails, and the woocommerce_addons_sections filter can
    return anything, so handle_legacy_marketplace_redirects() now only reads
    slugs from array entries that are objects with a string slug. With no
    valid sections the request falls back to the default extensions page.

    * Add changelog entry for legacy Extensions redirect fix

    * Reset the section query var after legacy redirect tests

    * Normalize sections to an array before collecting slugs

    * Redirect to the default Extensions page before loading categories

    ---------

    Co-authored-by: Thilina Pituwala <thilina.hasantha@gmail.com>

diff --git a/plugins/woocommerce/changelog/fix-wc-addons-redirect-sections-fatal b/plugins/woocommerce/changelog/fix-wc-addons-redirect-sections-fatal
new file mode 100644
index 00000000000..4607d08c6bc
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wc-addons-redirect-sections-fatal
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix a fatal error on the legacy Extensions page when WooCommerce.com categories can't be loaded.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php
index 33dbe25047a..6465bf44dcc 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php
@@ -196,13 +196,25 @@ class WC_Admin_Addons {
 			exit();
 		}

-		$sections         = self::get_sections();
-		$allowed_sections = array_map( fn( $section_object ) => $section_object->slug, $sections );
-		// Validate if the category is supported.
-		$section = in_array( $section, $allowed_sections, true ) ? $section : '_featured';
+		$default_redirect_url = admin_url( 'admin.php?page=wc-admin&path=%2Fextensions' );

 		if ( '_featured' === $section ) {
-			wp_safe_redirect( admin_url( 'admin.php?page=wc-admin&path=%2Fextensions' ) );
+			wp_safe_redirect( $default_redirect_url );
+			exit();
+		}
+
+		// get_sections() returns false when the request fails, and the filter can return anything.
+		$sections         = self::get_sections();
+		$sections         = is_array( $sections ) ? $sections : array();
+		$allowed_sections = array();
+		foreach ( $sections as $section_object ) {
+			if ( is_object( $section_object ) && isset( $section_object->slug ) && is_string( $section_object->slug ) ) {
+				$allowed_sections[] = $section_object->slug;
+			}
+		}
+
+		if ( ! in_array( $section, $allowed_sections, true ) ) {
+			wp_safe_redirect( $default_redirect_url );
 			exit();
 		}

diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-addons-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-addons-test.php
new file mode 100644
index 00000000000..958e062efe2
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-addons-test.php
@@ -0,0 +1,144 @@
+<?php
+declare( strict_types = 1 );
+
+/**
+ * Tests for WC_Admin_Addons.
+ *
+ * @package WooCommerce\Tests\Admin
+ */
+class WC_Admin_Addons_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * Location passed to the last intercepted redirect.
+	 *
+	 * @var string|null
+	 */
+	private $redirect_location = null;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		delete_transient( 'wc_addons_sections' );
+		add_filter( 'pre_http_request', fn() => new WP_Error( 'http_request_failed', 'cURL error 35' ) );
+		add_filter( 'wp_redirect', array( $this, 'intercept_redirect' ) );
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		unset( $_GET['section'] );
+		parent::tearDown();
+	}
+
+	/**
+	 * Intercepts redirects so the tested handler's trailing exit does not run.
+	 *
+	 * @param string $location Redirect target.
+	 * @return void
+	 * @throws RuntimeException Always.
+	 */
+	public function intercept_redirect( string $location ): void {
+		$this->redirect_location = $location;
+		throw new RuntimeException( 'Redirect intercepted.' );
+	}
+
+	/**
+	 * Runs the legacy redirect handler for a section and returns the redirect target.
+	 *
+	 * @param string $section Requested legacy section.
+	 * @return string
+	 */
+	private function get_redirect_for_section( string $section ): string {
+		$_GET['section']         = $section; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		$this->redirect_location = null;
+		try {
+			WC_Admin_Addons::handle_legacy_marketplace_redirects();
+		} catch ( RuntimeException $e ) {
+			// PHPUnit's converted warnings are RuntimeExceptions too; only swallow the redirect.
+			if ( null === $this->redirect_location ) {
+				throw $e;
+			}
+			return $this->redirect_location;
+		}
+		$this->fail( 'Expected the handler to redirect.' );
+	}
+
+	/**
+	 * @testdox Legacy redirect falls back to the extensions page when sections can't be loaded.
+	 */
+	public function test_redirect_falls_back_to_featured_when_sections_fail_to_load(): void {
+		$this->assertSame(
+			admin_url( 'admin.php?page=wc-admin&path=%2Fextensions' ),
+			$this->get_redirect_for_section( 'marketing' ),
+			'A failed sections request should send users to the default extensions page.'
+		);
+	}
+
+	/**
+	 * @testdox Legacy redirect sends the featured section to the extensions page without loading categories.
+	 */
+	public function test_redirect_for_featured_section_skips_categories_request(): void {
+		$http_requests = 0;
+		add_filter(
+			'pre_http_request',
+			function ( $response ) use ( &$http_requests ) {
+				++$http_requests;
+				return $response;
+			}
+		);
+
+		$this->assertSame(
+			admin_url( 'admin.php?page=wc-admin&path=%2Fextensions' ),
+			$this->get_redirect_for_section( '_featured' ),
+			'The featured section should go to the default extensions page.'
+		);
+		$this->assertSame( 0, $http_requests, 'The featured section should not request categories.' );
+
+		$this->get_redirect_for_section( 'marketing' );
+		$this->assertSame( 1, $http_requests, 'Other sections should still request categories.' );
+	}
+
+	/**
+	 * @testdox Legacy redirect ignores malformed filtered sections and keeps valid ones.
+	 */
+	public function test_redirect_ignores_malformed_sections(): void {
+		add_filter(
+			'woocommerce_addons_sections',
+			fn() => array(
+				'not-an-object',
+				null,
+				(object) array( 'label' => 'No slug' ),
+				(object) array( 'slug' => array( 'marketing' ) ),
+				(object) array( 'slug' => 'marketing' ),
+			)
+		);
+
+		$this->assertSame(
+			admin_url( 'admin.php?page=wc-admin&tab=extensions&path=%2Fextensions&category=marketing' ),
+			$this->get_redirect_for_section( 'marketing' ),
+			'The valid section should still resolve to its category.'
+		);
+	}
+
+	/**
+	 * @testdox Legacy redirect keeps known sections and drops unknown ones.
+	 */
+	public function test_redirect_keeps_known_sections_only(): void {
+		add_filter( 'woocommerce_addons_sections', fn() => array( (object) array( 'slug' => 'payment-gateways' ) ) );
+
+		$this->assertSame(
+			admin_url( 'admin.php?page=wc-admin&tab=extensions&path=%2Fextensions&category=payment-gateways' ),
+			$this->get_redirect_for_section( 'payment-gateways' ),
+			'A known section should be kept.'
+		);
+		$this->assertSame(
+			admin_url( 'admin.php?page=wc-admin&path=%2Fextensions' ),
+			$this->get_redirect_for_section( 'unknown' ),
+			'An unknown section should fall back to the default extensions page.'
+		);
+	}
+}