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