Commit 8dc2e595819 for woocommerce

commit 8dc2e5958196678c83ce894f213c19d9e1eaeb5f
Author: Ján Mikláš <neosinner@gmail.com>
Date:   Tue Aug 4 07:43:58 2026 +0200

    Fix Blueprint screen fatal when a WordPress.org API filter throws (#67342)

    * Fix Blueprint screen fatal when a WordPress.org API filter throws

    * Add changelog entry for Blueprint API filter fatal fix

    * Use the transient name constants in the Blueprint fallback tests

diff --git a/plugins/woocommerce/changelog/67121-fix-blueprint-themes-api-fatal b/plugins/woocommerce/changelog/67121-fix-blueprint-themes-api-fatal
new file mode 100644
index 00000000000..094f57cdd97
--- /dev/null
+++ b/plugins/woocommerce/changelog/67121-fix-blueprint-themes-api-fatal
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent the Blueprint settings screen from fataling when a third-party filter throws while WooCommerce requests plugin or theme metadata from WordPress.org.
diff --git a/plugins/woocommerce/src/Admin/Features/Blueprint/Init.php b/plugins/woocommerce/src/Admin/Features/Blueprint/Init.php
index 8f6e5bb57e1..48f8718090e 100644
--- a/plugins/woocommerce/src/Admin/Features/Blueprint/Init.php
+++ b/plugins/woocommerce/src/Admin/Features/Blueprint/Init.php
@@ -279,34 +279,40 @@ class Init {
 			$all_plugins[ $key ]['slug'] = $slug;
 		}

-		$api_response = $this->wp_plugins_api(
-			'plugin_information',
-			array(
-				'fields' => array(
-					'short_description' => false,
-					'sections'          => false,
-					'description'       => false,
-					'tested'            => false,
-					'requires'          => false,
-					'rating'            => false,
-					'ratings'           => false,
-					'downloaded'        => false,
-					'downloadlink'      => false,
-					'last_updated'      => false,
-					'added'             => false,
-					'tags'              => false,
-					'compatibility'     => false,
-					'homepage'          => false,
-					'versions'          => false,
-					'donate_link'       => false,
-					'reviews'           => false,
-					'banners'           => false,
-					'icons'             => false,
-					'active_installs'   => false,
-				),
-				'slugs'  => $plugin_slugs,
-			)
-		);
+		try {
+			$api_response = $this->wp_plugins_api(
+				'plugin_information',
+				array(
+					'fields' => array(
+						'short_description' => false,
+						'sections'          => false,
+						'description'       => false,
+						'tested'            => false,
+						'requires'          => false,
+						'rating'            => false,
+						'ratings'           => false,
+						'downloaded'        => false,
+						'downloadlink'      => false,
+						'last_updated'      => false,
+						'added'             => false,
+						'tags'              => false,
+						'compatibility'     => false,
+						'homepage'          => false,
+						'versions'          => false,
+						'donate_link'       => false,
+						'reviews'           => false,
+						'banners'           => false,
+						'icons'             => false,
+						'active_installs'   => false,
+					),
+					'slugs'  => $plugin_slugs,
+				)
+			);
+		} catch ( \Throwable $e ) {
+			// A third party filtering the plugins API result can throw. Don't let that take down the admin screen.
+			$this->log_api_failure( 'plugins_api', $e );
+			return $all_plugins;
+		}

 		// If API fails, return all plugins.
 		if ( is_wp_error( $api_response ) ) {
@@ -349,28 +355,34 @@ class Init {
 			}
 		}

-		$api_response = $this->wp_themes_api(
-			'theme_information',
-			array(
-				'fields' => array(
-					'downloadlink'    => true,
-					'sections'        => false,
-					'description'     => false,
-					'rating'          => false,
-					'ratings'         => false,
-					'downloaded'      => false,
-					'last_updated'    => false,
-					'tags'            => false,
-					'homepage'        => false,
-					'screenshots'     => false,
-					'screenshot_url'  => false,
-					'parent'          => false,
-					'versions'        => false,
-					'extended_author' => false,
-				),
-				'slugs'  => $theme_slugs,
-			)
-		);
+		try {
+			$api_response = $this->wp_themes_api(
+				'theme_information',
+				array(
+					'fields' => array(
+						'downloadlink'    => true,
+						'sections'        => false,
+						'description'     => false,
+						'rating'          => false,
+						'ratings'         => false,
+						'downloaded'      => false,
+						'last_updated'    => false,
+						'tags'            => false,
+						'homepage'        => false,
+						'screenshots'     => false,
+						'screenshot_url'  => false,
+						'parent'          => false,
+						'versions'        => false,
+						'extended_author' => false,
+					),
+					'slugs'  => $theme_slugs,
+				)
+			);
+		} catch ( \Throwable $e ) {
+			// A third party filtering the themes API result can throw. Don't let that take down the admin screen.
+			$this->log_api_failure( 'themes_api', $e );
+			return $all_themes;
+		}

 		// If the API fails, return all installed themes.
 		if ( is_wp_error( $api_response ) ) {
@@ -388,4 +400,25 @@ class Init {
 		set_transient( self::INSTALLED_WP_ORG_THEMES_TRANSIENT, $wp_org_themes );
 		return $wp_org_themes;
 	}
+
+	/**
+	 * Log a WordPress.org API request that threw.
+	 *
+	 * @param string     $api The API that was called, for example "themes_api".
+	 * @param \Throwable $e   The thrown error.
+	 *
+	 * @return void
+	 */
+	private function log_api_failure( string $api, \Throwable $e ) {
+		wc_get_logger()->error(
+			sprintf(
+				'Blueprint: %1$s() threw "%2$s" in %3$s:%4$d. Falling back to the installed list.',
+				$api,
+				$e->getMessage(),
+				$e->getFile(),
+				$e->getLine()
+			),
+			array( 'source' => 'blueprint' )
+		);
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php
index a5856a2ac74..8a82bb3d92e 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php
@@ -129,6 +129,52 @@ class InitTest extends MockeryTestCase {
 		$this->assertSame( $expected, $result );
 	}

+	/**
+	 * A third party filtering plugins_api can throw. The export group should still list the installed plugins.
+	 */
+	public function test_get_plugins_for_export_group_falls_back_when_plugins_api_throws() {
+		delete_transient( $this->init::INSTALLED_WP_ORG_PLUGINS_TRANSIENT );
+
+		$mock_plugins = array(
+			'plugin-1/plugin.php' => array( 'Name' => 'Plugin One' ),
+			'plugin-2/plugin.php' => array( 'Name' => 'Plugin Two' ),
+		);
+
+		$this->init->shouldReceive( 'wp_get_plugins' )->andReturn( $mock_plugins );
+		$this->init->shouldReceive( 'wp_get_option' )->andReturn( array( 'plugin-1/plugin.php' ) );
+		$this->init->shouldReceive( 'wp_plugins_api' )->once()->andThrow( new \TypeError( 'Argument #1 ($slug) must be of type string, null given' ) );
+
+		$result = $this->init->get_plugins_for_export_group();
+
+		$this->assertSame( array( 'plugin-1/plugin.php', 'plugin-2/plugin.php' ), wp_list_pluck( $result, 'id' ) );
+		$this->assertFalse( get_transient( $this->init::INSTALLED_WP_ORG_PLUGINS_TRANSIENT ) );
+	}
+
+	/**
+	 * A third party filtering themes_api can throw. The export group should still list the installed themes.
+	 */
+	public function test_get_themes_for_export_group_falls_back_when_themes_api_throws() {
+		delete_transient( $this->init::INSTALLED_WP_ORG_THEMES_TRANSIENT );
+
+		$mock_theme_1      = $this->createThemeStub( 'theme-one', 'Theme One' );
+		$mock_theme_2      = $this->createThemeStub( 'custom-theme', 'Custom Theme' );
+		$mock_active_theme = $this->createThemeStub( 'theme-one', 'Theme One' );
+
+		$this->init->shouldReceive( 'wp_get_themes' )->andReturn(
+			array(
+				'theme-one'    => $mock_theme_1,
+				'custom-theme' => $mock_theme_2,
+			)
+		);
+		$this->init->shouldReceive( 'wp_get_theme' )->andReturn( $mock_active_theme );
+		$this->init->shouldReceive( 'wp_themes_api' )->once()->andThrow( new \TypeError( 'Argument #1 ($slug) must be of type string, null given' ) );
+
+		$result = $this->init->get_themes_for_export_group();
+
+		$this->assertSame( array( 'theme-one', 'custom-theme' ), wp_list_pluck( $result, 'id' ) );
+		$this->assertFalse( get_transient( $this->init::INSTALLED_WP_ORG_THEMES_TRANSIENT ) );
+	}
+
 	/**
 	 * Test the get_step_groups_for_js method.
 	 */