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.
*/