From a0a7cccd6a364b566e86bdfe0b8c2dae4d6f2f8b Mon Sep 17 00:00:00 2001
From: MohammadReza Alidoosti <34165020+Mr-Alidoosti@users.noreply.github.com>
Date: Sun, 4 Oct 2026 14:29:07 +0330
Subject: [PATCH 1/2] Upgrade/Install: Integrate closed, suspended, and
outdated plugins/themes API data. See #66239.
---
src/js/_enqueues/wp/theme.js | 8 +
src/wp-admin/css/list-tables.css | 27 +
src/wp-admin/css/themes.css | 39 ++
.../includes/class-wp-plugins-list-table.php | 44 +-
.../includes/class-wp-site-health.php | 296 ++++++++++
src/wp-admin/includes/plugin-install.php | 138 ++++-
src/wp-admin/includes/theme-install.php | 142 +++++
src/wp-admin/includes/theme.php | 42 +-
src/wp-admin/includes/update.php | 121 ++++-
src/wp-admin/themes.php | 122 ++++-
src/wp-includes/update.php | 59 ++
tests/phpunit/tests/plugins/closedPlugins.php | 504 ++++++++++++++++++
tests/phpunit/tests/theme/closedThemes.php | 285 ++++++++++
13 files changed, 1796 insertions(+), 31 deletions(-)
create mode 100644 tests/phpunit/tests/plugins/closedPlugins.php
create mode 100644 tests/phpunit/tests/theme/closedThemes.php
diff --git a/src/js/_enqueues/wp/theme.js b/src/js/_enqueues/wp/theme.js
index ff5d72ad2801e..1c71990d42dfe 100644
--- a/src/js/_enqueues/wp/theme.js
+++ b/src/js/_enqueues/wp/theme.js
@@ -431,6 +431,10 @@ themes.view.Theme = wp.Backbone.View.extend({
// Renders active theme styles.
this.activeTheme();
+ this.$el.toggleClass( 'closed', !! ( this.model.get( 'closed' ) || this.model.get( 'is_suspended' ) ) );
+ this.$el.toggleClass( 'outdated', !! this.model.get( 'is_outdated' ) );
+ this.$el.toggleClass( 'security-issue', !! this.model.get( 'is_security' ) );
+
if ( this.model.get( 'displayAuthor' ) ) {
this.$el.addClass( 'display-author' );
}
@@ -708,6 +712,10 @@ themes.view.Details = wp.Backbone.View.extend({
this.$el.html( this.html( data ) );
// Renders active theme styles.
this.activeTheme();
+
+ this.$el.toggleClass( 'closed', !! ( this.model.get( 'closed' ) || this.model.get( 'is_suspended' ) ) );
+ this.$el.toggleClass( 'outdated', !! this.model.get( 'is_outdated' ) );
+ this.$el.toggleClass( 'security-issue', !! this.model.get( 'is_security' ) );
// Set up navigation events.
this.navigation();
// Checks screenshot size.
diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css
index f6105ad5a59fb..eb05b85baf0cb 100644
--- a/src/wp-admin/css/list-tables.css
+++ b/src/wp-admin/css/list-tables.css
@@ -1349,6 +1349,33 @@ ul.cat-checklist input[name="post_category[]"]:indeterminate::before {
white-space: nowrap;
}
+.plugins .plugin-status-badge {
+ display: inline-block;
+ font-size: 11px;
+ line-height: 1.4;
+ font-weight: 600;
+ padding: 1px 6px;
+ border-radius: 3px;
+ margin-left: 6px;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+
+.plugins .plugin-status-badge-closed {
+ background-color: #d63638;
+ color: #fff;
+}
+
+.plugins .plugin-status-badge-outdated {
+ background-color: #dba617;
+ color: #1d2327;
+}
+
+[dir="rtl"] .plugins .plugin-status-badge {
+ margin-left: 0;
+ margin-right: 6px;
+}
+
.plugins .plugin-title .dashicons,
.plugins .plugin-title img.plugin-icon,
.plugins .plugin-title img.updates-table-screenshot {
diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css
index 5b71c223b6ce6..ff7540248cf0d 100644
--- a/src/wp-admin/css/themes.css
+++ b/src/wp-admin/css/themes.css
@@ -87,6 +87,45 @@ body.js .theme-browser.search-loading {
background: #fff;
}
+.theme-browser .theme .theme-status-badge {
+ display: inline-block;
+ font-size: 11px;
+ line-height: 1.4;
+ font-weight: 600;
+ padding: 1px 6px;
+ border-radius: 3px;
+ margin-left: 6px;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+
+.theme-browser .theme .theme-status-badge-closed {
+ background-color: #d63638;
+ color: #fff;
+}
+
+.theme-browser .theme .theme-status-badge-outdated {
+ background-color: #dba617;
+ color: #1d2327;
+}
+
+[dir="rtl"] .theme-browser .theme .theme-status-badge {
+ margin-left: 0;
+ margin-right: 6px;
+}
+
+.theme-browser .theme.closed {
+ border-color: #d63638;
+}
+
+.theme-browser .theme.outdated {
+ border-color: #dba617;
+}
+
+.theme-browser .theme .theme-status-notice {
+ z-index: 10;
+}
+
/* Activate and Customize buttons, shown on hover and focus */
.theme-browser .theme .theme-actions {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php
index d8945e103064e..34b5306d1c94a 100644
--- a/src/wp-admin/includes/class-wp-plugins-list-table.php
+++ b/src/wp-admin/includes/class-wp-plugins-list-table.php
@@ -1195,15 +1195,46 @@ public function single_row( $item ) {
$plugin_name = $plugin_data['Name'];
}
+ $is_plugin_closed = ! empty( $plugin_data['closed'] ) || 'closed' === ( $plugin_data['status'] ?? '' ) || 'disabled' === ( $plugin_data['status'] ?? '' );
+ $is_plugin_security = ! empty( $plugin_data['is_security'] ) || 'security-issue' === ( $plugin_data['reason'] ?? '' ) || 'security-issue' === ( $plugin_data['closed_reason'] ?? '' );
+ $is_plugin_outdated = ! empty( $plugin_data['is_outdated'] );
+
+ if ( ! $is_plugin_closed && ! $is_plugin_outdated ) {
+ $update_plugins_transient = get_site_transient( 'update_plugins' );
+ if ( is_object( $update_plugins_transient ) ) {
+ $transient_item = null;
+ if ( isset( $update_plugins_transient->response[ $plugin_file ] ) ) {
+ $transient_item = (object) $update_plugins_transient->response[ $plugin_file ];
+ } elseif ( isset( $update_plugins_transient->no_update[ $plugin_file ] ) ) {
+ $transient_item = (object) $update_plugins_transient->no_update[ $plugin_file ];
+ }
+ if ( $transient_item ) {
+ $is_plugin_closed = ! empty( $transient_item->closed ) || 'closed' === ( $transient_item->status ?? '' ) || 'disabled' === ( $transient_item->status ?? '' );
+ $is_plugin_security = ! empty( $transient_item->is_security ) || 'security-issue' === ( $transient_item->reason ?? '' ) || 'security-issue' === ( $transient_item->closed_reason ?? '' );
+ $is_plugin_outdated = ! empty( $transient_item->is_outdated );
+ }
+ }
+ }
+
if (
! empty( $totals['upgrade'] ) &&
! empty( $plugin_data['update'] ) ||
! $compatible_php ||
- ! $compatible_wp
+ ! $compatible_wp ||
+ $is_plugin_closed ||
+ $is_plugin_outdated
) {
$class .= ' update';
}
+ if ( $is_plugin_closed ) {
+ $class .= ' closed';
+ }
+
+ if ( $is_plugin_outdated ) {
+ $class .= ' outdated';
+ }
+
$paused = ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file );
if ( $paused ) {
@@ -1236,7 +1267,16 @@ public function single_row( $item ) {
echo "
' .
'' .
- '',
+ ' ',
$active_class,
esc_attr( $plugin_slug . '-update' ),
esc_attr( $plugin_slug ),
esc_attr( $file ),
esc_attr( $wp_list_table->get_column_count() ),
- $notice_type
+ $notice_type,
+ $role_attr
);
- if ( ! current_user_can( 'update_plugins' ) ) {
+ if ( $is_closed ) {
+ $closed_date = '';
+ if ( ! empty( $response->closed_date ) ) {
+ $closed_timestamp = strtotime( $response->closed_date );
+ $closed_date = $closed_timestamp ? wp_date( get_option( 'date_format' ), $closed_timestamp ) : $response->closed_date;
+ }
+
+ $reason = $response->reason_text ?? ( $response->closed_reason ?? ( $response->reason ?? '' ) );
+
+ if ( $is_security ) {
+ if ( $closed_date ) {
+ printf(
+ /* translators: %s: Plugin closure date. */
+ __( 'Warning: This plugin was closed on %s due to a security issue and is no longer available for download. It should be uninstalled or replaced immediately.' ),
+ esc_html( $closed_date )
+ );
+ } else {
+ _e( 'Warning: This plugin was closed due to a security issue and is no longer available for download. It should be uninstalled or replaced immediately.' );
+ }
+ } else {
+ if ( $closed_date && $reason ) {
+ printf(
+ /* translators: 1: Plugin closure date, 2: Plugin closure reason. */
+ __( 'Notice: This plugin was closed on %1$s (%2$s) and is no longer available for download.' ),
+ esc_html( $closed_date ),
+ esc_html( $reason )
+ );
+ } elseif ( $closed_date ) {
+ printf(
+ /* translators: %s: Plugin closure date. */
+ __( 'Notice: This plugin was closed on %s and is no longer available for download.' ),
+ esc_html( $closed_date )
+ );
+ } elseif ( $reason ) {
+ printf(
+ /* translators: %s: Plugin closure reason. */
+ __( 'Notice: This plugin was closed (%s) and is no longer available for download.' ),
+ esc_html( $reason )
+ );
+ } else {
+ _e( 'Notice: This plugin was closed and is no longer available for download.' );
+ }
+ }
+ } elseif ( $is_outdated && ! isset( $current->response[ $file ] ) ) {
+ if ( ! empty( $response->outdated_notice ) ) {
+ echo esc_html( $response->outdated_notice );
+ } else {
+ _e( 'This plugin has not been tested with the latest 3 major releases of WordPress and may no longer be maintained.' );
+ }
+ } elseif ( ! current_user_can( 'update_plugins' ) ) {
printf(
/* translators: 1: Plugin name, 2: Details URL, 3: Additional link attributes, 4: Version number. */
__( 'There is a new version of %1$s available. View version %4$s details.' ),
@@ -582,6 +674,15 @@ function wp_plugin_update_row( $file, $plugin_data ) {
}
}
+ if ( ! $is_closed && $is_outdated && isset( $current->response[ $file ] ) ) {
+ echo ' ';
+ if ( ! empty( $response->outdated_notice ) ) {
+ echo esc_html( $response->outdated_notice );
+ } else {
+ _e( 'This plugin has not been tested with the latest 3 major releases of WordPress and may no longer be maintained.' );
+ }
+ }
+
/**
* Fires at the end of the update message container in each
* row of the plugins list table.
diff --git a/src/wp-admin/themes.php b/src/wp-admin/themes.php
index ac2cd4a9824cb..c400971fa304e 100644
--- a/src/wp-admin/themes.php
+++ b/src/wp-admin/themes.php
@@ -444,7 +444,37 @@
-
+
+ 'error',
+ 'additional_classes' => array( 'notice-alt', 'inline', 'theme-status-notice' ),
+ )
+ );
+ } else {
+ wp_admin_notice(
+ __( 'Suspended / Closed: This theme has been removed from the WordPress Theme Directory.' ),
+ array(
+ 'type' => 'warning',
+ 'additional_classes' => array( 'notice-alt', 'inline', 'theme-status-notice' ),
+ )
+ );
+ }
+ ?>
+
+ 'warning',
+ 'additional_classes' => array( 'notice-alt', 'inline', 'theme-status-notice' ),
+ )
+ );
+ ?>
+
+ ' . __( 'Theme status:' ) . ' ' . esc_html( $badge_label ) . '';
+ } elseif ( ! empty( $theme['is_outdated'] ) ) {
+ $theme_badge = ' ' . __( 'Theme status:' ) . ' ' . __( 'Outdated' ) . '';
+ }
+ ?>
-
+
-
+
@@ -842,7 +881,21 @@ function wp_theme_auto_update_setting_template() {
<# } #>
- <# if ( data.hasUpdate ) { #>
+ <# if ( data.closed || data.is_suspended ) { #>
+ <# if ( data.is_security ) { #>
+
+ <# } else { #>
+
+ <# } #>
+ <# } else if ( data.is_outdated ) { #>
+
+ <# } else if ( data.hasUpdate ) { #>
<# if ( data.updateResponse.compatibleWP && data.updateResponse.compatiblePHP ) { #>
<# if ( data.hasPackage ) { #>
@@ -989,12 +1042,18 @@ function wp_theme_auto_update_setting_template() {
+ <# var themeBadge = ''; #>
+ <# if ( data.closed || data.is_suspended ) { #>
+ <# themeBadge = ' ' + ( data.is_security ? '' : '' ) + ''; #>
+ <# } else if ( data.is_outdated ) { #>
+ <# themeBadge = ' '; #>
+ <# } #>
<# if ( data.active ) { #>
- {{{ data.name }}}
+ {{{ data.name }}}{{{ themeBadge }}}
<# } else { #>
- {{{ data.name }}}
+ {{{ data.name }}}{{{ themeBadge }}}
<# } #>
@@ -1154,6 +1213,57 @@ function wp_theme_auto_update_setting_template() {
<# } #>
+ <# if ( data.closed || data.is_suspended ) { #>
+ <# if ( data.is_security ) { #>
+
+
+
+ <# if ( data.closedDate ) { #>
+
+ <# } else { #>
+
+ <# } #>
+
+
+ <# } else { #>
+
+
+
+ <# if ( data.closedDate && data.closedReason ) { #>
+
+ <# } else { #>
+
+ <# } #>
+
+
+ <# } #>
+ <# } #>
+
+ <# if ( data.is_outdated ) { #>
+
+
+ <# if ( data.outdatedNotice ) { #>
+ {{{ data.outdatedNotice }}}
+ <# } else { #>
+
+ <# } #>
+
+
+ <# } #>
+
<# if ( data.hasUpdate ) { #>
<# if ( data.updateResponse.compatibleWP && data.updateResponse.compatiblePHP ) { #>
diff --git a/src/wp-includes/update.php b/src/wp-includes/update.php
index 9f4257ab03da6..710b18a691b29 100644
--- a/src/wp-includes/update.php
+++ b/src/wp-includes/update.php
@@ -503,6 +503,14 @@ function wp_update_plugins( $extra_stats = array() ) {
$updates->response = $response['plugins'];
$updates->translations = $response['translations'];
$updates->no_update = $response['no_update'];
+
+ if ( isset( $response['closed'] ) && is_array( $response['closed'] ) ) {
+ foreach ( $response['closed'] as $closed_plugin => $closed_data ) {
+ if ( ! isset( $updates->response[ $closed_plugin ] ) && ! isset( $updates->no_update[ $closed_plugin ] ) ) {
+ $updates->no_update[ $closed_plugin ] = (object) $closed_data;
+ }
+ }
+ }
}
// Support updates for any plugins using the `Update URI` header field.
@@ -600,6 +608,16 @@ function wp_update_plugins( $extra_stats = array() ) {
unset( $item->translations, $item->compatibility );
+ if ( isset( $item->closed ) ) {
+ $item->closed = (bool) $item->closed;
+ }
+ if ( isset( $item->is_outdated ) ) {
+ $item->is_outdated = (bool) $item->is_outdated;
+ }
+ if ( isset( $item->is_security ) ) {
+ $item->is_security = (bool) $item->is_security;
+ }
+
return $item;
};
@@ -784,6 +802,14 @@ function wp_update_themes( $extra_stats = array() ) {
$new_update->response = $response['themes'];
$new_update->no_update = $response['no_update'];
$new_update->translations = $response['translations'];
+
+ if ( isset( $response['closed'] ) && is_array( $response['closed'] ) ) {
+ foreach ( $response['closed'] as $closed_theme => $closed_data ) {
+ if ( ! isset( $new_update->response[ $closed_theme ] ) && ! isset( $new_update->no_update[ $closed_theme ] ) ) {
+ $new_update->no_update[ $closed_theme ] = (array) $closed_data;
+ }
+ }
+ }
}
// Support updates for any themes using the `Update URI` header field.
@@ -872,6 +898,39 @@ function wp_update_themes( $extra_stats = array() ) {
}
}
+ $sanitize_theme_update_payload = static function ( &$item ) {
+ if ( is_object( $item ) ) {
+ $item = (array) $item;
+ }
+
+ if ( is_array( $item ) ) {
+ if ( isset( $item['closed'] ) ) {
+ $item['closed'] = (bool) $item['closed'];
+ }
+ if ( isset( $item['is_closed'] ) ) {
+ $item['is_closed'] = (bool) $item['is_closed'];
+ }
+ if ( isset( $item['is_suspended'] ) ) {
+ $item['is_suspended'] = (bool) $item['is_suspended'];
+ }
+ if ( isset( $item['is_outdated'] ) ) {
+ $item['is_outdated'] = (bool) $item['is_outdated'];
+ }
+ if ( isset( $item['is_security'] ) ) {
+ $item['is_security'] = (bool) $item['is_security'];
+ }
+ }
+
+ return $item;
+ };
+
+ if ( is_array( $new_update->response ) ) {
+ array_walk( $new_update->response, $sanitize_theme_update_payload );
+ }
+ if ( is_array( $new_update->no_update ) ) {
+ array_walk( $new_update->no_update, $sanitize_theme_update_payload );
+ }
+
set_site_transient( 'update_themes', $new_update );
}
diff --git a/tests/phpunit/tests/plugins/closedPlugins.php b/tests/phpunit/tests/plugins/closedPlugins.php
new file mode 100644
index 0000000000000..b98b3c6677ce9
--- /dev/null
+++ b/tests/phpunit/tests/plugins/closedPlugins.php
@@ -0,0 +1,504 @@
+ array(
+ 'Name' => 'Security Plugin',
+ 'Version' => '1.0.0',
+ 'PluginURI' => 'https://wordpress.org/plugins/sec-plugin/',
+ 'Author' => 'Tester',
+ 'TextDomain' => 'sec-plugin',
+ 'UpdateURI' => '',
+ ),
+ 'outdated/outdated.php' => array(
+ 'Name' => 'Outdated Plugin',
+ 'Version' => '2.0.0',
+ 'PluginURI' => 'https://wordpress.org/plugins/outdated/',
+ 'Author' => 'Tester',
+ 'TextDomain' => 'outdated',
+ 'UpdateURI' => '',
+ ),
+ 'general-closed/general.php' => array(
+ 'Name' => 'General Closed Plugin',
+ 'Version' => '1.5.0',
+ 'PluginURI' => 'https://wordpress.org/plugins/general-closed/',
+ 'Author' => 'Tester',
+ 'TextDomain' => 'general-closed',
+ 'UpdateURI' => '',
+ ),
+ );
+
+ add_filter(
+ 'all_plugins',
+ static function () use ( $test_plugins ) {
+ return $test_plugins;
+ }
+ );
+
+ add_filter(
+ 'pre_http_request',
+ static function ( $response, $parsed_args, $url ) {
+ if ( false === strpos( $url, 'api.wordpress.org/plugins/update-check' ) ) {
+ return $response;
+ }
+
+ $mock_body = array(
+ 'plugins' => array(
+ 'sec-plugin/sec-plugin.php' => (object) array(
+ 'id' => 'w.org/plugins/sec-plugin',
+ 'slug' => 'sec-plugin',
+ 'plugin' => 'sec-plugin/sec-plugin.php',
+ 'new_version' => '1.1.0',
+ 'url' => 'https://wordpress.org/plugins/sec-plugin/',
+ 'package' => '',
+ 'closed' => true,
+ 'closed_date' => '2025-01-15',
+ 'closed_reason' => 'security-issue',
+ 'reason' => 'security-issue',
+ 'reason_text' => 'Security Issue',
+ 'is_security' => true,
+ 'is_outdated' => false,
+ ),
+ ),
+ 'translations' => array(),
+ 'no_update' => array(
+ 'outdated/outdated.php' => (object) array(
+ 'id' => 'w.org/plugins/outdated',
+ 'slug' => 'outdated',
+ 'plugin' => 'outdated/outdated.php',
+ 'new_version' => '2.0.0',
+ 'url' => 'https://wordpress.org/plugins/outdated/',
+ 'is_outdated' => true,
+ 'outdated_notice' => 'This plugin has not been tested with the latest 3 major releases.',
+ 'closed' => false,
+ ),
+ ),
+ 'closed' => array(
+ 'general-closed/general.php' => (object) array(
+ 'id' => 'w.org/plugins/general-closed',
+ 'slug' => 'general-closed',
+ 'plugin' => 'general-closed/general.php',
+ 'new_version' => '1.5.0',
+ 'url' => 'https://wordpress.org/plugins/general-closed/',
+ 'closed' => true,
+ 'closed_date' => '2024-06-20',
+ 'closed_reason' => 'author-request',
+ 'reason' => 'author-request',
+ 'reason_text' => 'Author Request',
+ 'is_security' => false,
+ 'is_outdated' => false,
+ ),
+ ),
+ );
+
+ return array(
+ 'headers' => array(),
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'body' => wp_json_encode( $mock_body ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ delete_site_transient( 'update_plugins' );
+ wp_update_plugins( array( 'core' => 'test' ) );
+
+ $transient = get_site_transient( 'update_plugins' );
+ $this->assertIsObject( $transient, 'Transient should be an object.' );
+ $this->assertArrayHasKey( 'sec-plugin/sec-plugin.php', $transient->response );
+
+ $sec_item = $transient->response['sec-plugin/sec-plugin.php'];
+ $this->assertTrue( $sec_item->closed );
+ $this->assertTrue( $sec_item->is_security );
+ $this->assertSame( '2025-01-15', $sec_item->closed_date );
+ $this->assertSame( 'security-issue', $sec_item->closed_reason );
+
+ $this->assertArrayHasKey( 'outdated/outdated.php', $transient->no_update );
+ $outdated_item = $transient->no_update['outdated/outdated.php'];
+ $this->assertTrue( $outdated_item->is_outdated );
+ $this->assertSame( 'This plugin has not been tested with the latest 3 major releases.', $outdated_item->outdated_notice );
+
+ $this->assertArrayHasKey( 'general-closed/general.php', $transient->no_update );
+ $closed_item = $transient->no_update['general-closed/general.php'];
+ $this->assertTrue( $closed_item->closed );
+ $this->assertFalse( $closed_item->is_security );
+ $this->assertSame( '2024-06-20', $closed_item->closed_date );
+ $this->assertSame( 'author-request', $closed_item->closed_reason );
+ }
+
+ /**
+ * Tests that wp_plugin_update_row outputs a critical security alert notice for security closures.
+ *
+ * @covers ::wp_plugin_update_row
+ */
+ public function test_wp_plugin_update_row_security_closure() {
+ require_once ABSPATH . 'wp-admin/includes/update.php';
+
+ $file = 'sec-plugin/sec-plugin.php';
+ $plugin_data = array(
+ 'Name' => 'Security Plugin',
+ 'Version' => '1.0.0',
+ );
+
+ $transient = new stdClass();
+ $transient->response = array(
+ $file => (object) array(
+ 'slug' => 'sec-plugin',
+ 'new_version' => '1.0.0',
+ 'closed' => true,
+ 'closed_date' => '2025-01-15',
+ 'closed_reason' => 'security-issue',
+ 'is_security' => true,
+ ),
+ );
+ $transient->no_update = array();
+ set_site_transient( 'update_plugins', $transient );
+
+ ob_start();
+ wp_plugin_update_row( $file, $plugin_data );
+ $output = ob_get_clean();
+
+ $this->assertStringContainsString( 'notice-error', $output );
+ $this->assertStringContainsString( 'role="alert"', $output );
+ $this->assertStringContainsString( 'Warning: This plugin was closed on', $output );
+ $this->assertStringContainsString( 'due to a security issue and is no longer available for download. It should be uninstalled or replaced immediately.', $output );
+ }
+
+ /**
+ * Tests that wp_plugin_update_row outputs a warning notice for general closures.
+ *
+ * @covers ::wp_plugin_update_row
+ */
+ public function test_wp_plugin_update_row_general_closure() {
+ require_once ABSPATH . 'wp-admin/includes/update.php';
+
+ $file = 'closed-plugin/closed-plugin.php';
+ $plugin_data = array(
+ 'Name' => 'Closed Plugin',
+ 'Version' => '1.0.0',
+ );
+
+ $transient = new stdClass();
+ $transient->response = array();
+ $transient->no_update = array(
+ $file => (object) array(
+ 'slug' => 'closed-plugin',
+ 'new_version' => '1.0.0',
+ 'closed' => true,
+ 'closed_date' => '2024-06-20',
+ 'reason_text' => 'Author Request',
+ 'is_security' => false,
+ ),
+ );
+ set_site_transient( 'update_plugins', $transient );
+
+ ob_start();
+ wp_plugin_update_row( $file, $plugin_data );
+ $output = ob_get_clean();
+
+ $this->assertStringContainsString( 'notice-warning', $output );
+ $this->assertStringContainsString( 'role="status"', $output );
+ $this->assertStringContainsString( 'Notice: This plugin was closed on', $output );
+ $this->assertStringContainsString( 'Author Request', $output );
+ $this->assertStringContainsString( 'and is no longer available for download.', $output );
+ }
+
+ /**
+ * Tests that wp_plugin_update_row outputs a warning notice for outdated plugins.
+ *
+ * @covers ::wp_plugin_update_row
+ */
+ public function test_wp_plugin_update_row_outdated() {
+ require_once ABSPATH . 'wp-admin/includes/update.php';
+
+ $file = 'outdated-plugin/outdated-plugin.php';
+ $plugin_data = array(
+ 'Name' => 'Outdated Plugin',
+ 'Version' => '1.0.0',
+ );
+
+ $transient = new stdClass();
+ $transient->response = array();
+ $transient->no_update = array(
+ $file => (object) array(
+ 'slug' => 'outdated-plugin',
+ 'new_version' => '1.0.0',
+ 'is_outdated' => true,
+ 'outdated_notice' => 'Custom outdated warning notice.',
+ ),
+ );
+ set_site_transient( 'update_plugins', $transient );
+
+ ob_start();
+ wp_plugin_update_row( $file, $plugin_data );
+ $output = ob_get_clean();
+
+ $this->assertStringContainsString( 'notice-warning', $output );
+ $this->assertStringContainsString( 'role="status"', $output );
+ $this->assertStringContainsString( 'Custom outdated warning notice.', $output );
+ }
+
+ /**
+ * Tests that WP_Plugins_List_Table renders badges for closed and outdated plugins.
+ *
+ * @covers WP_Plugins_List_Table::single_row
+ */
+ public function test_wp_plugins_list_table_badges() {
+ require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
+ require_once ABSPATH . 'wp-admin/includes/screen.php';
+ require_once ABSPATH . 'wp-admin/includes/template.php';
+ require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
+ require_once ABSPATH . 'wp-admin/includes/class-wp-plugins-list-table.php';
+
+ set_current_screen( 'plugins.php' );
+
+ $transient = new stdClass();
+ $transient->response = array(
+ 'sec/sec.php' => (object) array(
+ 'slug' => 'sec',
+ 'closed' => true,
+ 'is_security' => true,
+ ),
+ );
+ $transient->no_update = array(
+ 'gen/gen.php' => (object) array(
+ 'slug' => 'gen',
+ 'closed' => true,
+ 'is_security' => false,
+ ),
+ 'old/old.php' => (object) array(
+ 'slug' => 'old',
+ 'is_outdated' => true,
+ ),
+ );
+ set_site_transient( 'update_plugins', $transient );
+
+ $table = new WP_Plugins_List_Table();
+
+ // Security closed plugin.
+ ob_start();
+ $table->single_row(
+ array(
+ 'sec/sec.php',
+ array(
+ 'Name' => 'Security Plugin',
+ 'Version' => '1.0.0',
+ 'Description' => 'Test',
+ ),
+ )
+ );
+ $output_sec = ob_get_clean();
+ $this->assertStringContainsString( 'plugin-status-badge-security', $output_sec );
+ $this->assertStringContainsString( 'Closed (Security)', $output_sec );
+
+ // General closed plugin.
+ ob_start();
+ $table->single_row(
+ array(
+ 'gen/gen.php',
+ array(
+ 'Name' => 'General Plugin',
+ 'Version' => '1.0.0',
+ 'Description' => 'Test',
+ ),
+ )
+ );
+ $output_gen = ob_get_clean();
+ $this->assertStringContainsString( 'plugin-status-badge plugin-status-badge-closed', $output_gen );
+ $this->assertStringContainsString( 'Closed', $output_gen );
+
+ // Outdated plugin.
+ ob_start();
+ $table->single_row(
+ array(
+ 'old/old.php',
+ array(
+ 'Name' => 'Old Plugin',
+ 'Version' => '1.0.0',
+ 'Description' => 'Test',
+ ),
+ )
+ );
+ $output_old = ob_get_clean();
+ $this->assertStringContainsString( 'plugin-status-badge plugin-status-badge-outdated', $output_old );
+ $this->assertStringContainsString( 'Outdated', $output_old );
+ }
+
+ /**
+ * Tests that plugins_api handles error => closed without converting to WP_Error.
+ *
+ * @covers ::plugins_api
+ */
+ public function test_plugins_api_preserves_closed_plugin_object() {
+ require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
+
+ add_filter(
+ 'pre_http_request',
+ static function ( $response, $parsed_args, $url ) {
+ if ( false === strpos( $url, 'api.wordpress.org/plugins/info' ) ) {
+ return $response;
+ }
+
+ return array(
+ 'headers' => array(),
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'body' => wp_json_encode(
+ array(
+ 'error' => 'closed',
+ 'name' => 'Test Closed Plugin',
+ 'slug' => 'test-closed-plugin',
+ 'description' => 'This plugin has been closed.',
+ 'status' => 'closed',
+ 'closed' => true,
+ 'closed_date' => '2025-01-15',
+ 'reason' => 'security-issue',
+ 'reason_text' => 'Security Issue',
+ 'is_security' => true,
+ 'is_outdated' => false,
+ )
+ ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ $result = plugins_api( 'plugin_information', array( 'slug' => 'test-closed-plugin' ) );
+
+ $this->assertNotWPError( $result );
+ $this->assertIsObject( $result );
+ $this->assertSame( 'closed', $result->error );
+ $this->assertTrue( $result->closed );
+ $this->assertTrue( $result->is_security );
+ $this->assertSame( 'security-issue', $result->reason );
+ }
+
+ /**
+ * Tests Site Health check for closed plugins.
+ *
+ * @covers WP_Site_Health::get_test_closed_plugins_and_themes
+ */
+ public function test_site_health_closed_plugins_check() {
+ require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+
+ $mock_plugins = array(
+ 'safe-plugin/safe-plugin.php' => array(
+ 'Name' => 'Safe Plugin',
+ 'Version' => '1.0.0',
+ ),
+ 'sec-plugin/sec-plugin.php' => array(
+ 'Name' => 'Security Plugin',
+ 'Version' => '1.0.0',
+ ),
+ 'closed-plugin/closed-plugin.php' => array(
+ 'Name' => 'Closed Plugin',
+ 'Version' => '1.0.0',
+ ),
+ 'old-plugin/old-plugin.php' => array(
+ 'Name' => 'Old Plugin',
+ 'Version' => '1.0.0',
+ ),
+ );
+
+ wp_cache_set( 'plugins', array( '' => $mock_plugins ), 'plugins' );
+
+ $site_health = new WP_Site_Health();
+
+ // Case 1: No closed active plugins -> good status.
+ update_option( 'active_plugins', array( 'safe-plugin/safe-plugin.php' ) );
+ delete_site_transient( 'update_plugins' );
+
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'good', $res['status'] );
+
+ // Case 2: Active plugin closed for security -> critical status.
+ $transient = new stdClass();
+ $transient->response = array(
+ 'sec-plugin/sec-plugin.php' => (object) array(
+ 'slug' => 'sec-plugin',
+ 'closed' => true,
+ 'is_security' => true,
+ 'closed_date' => '2025-01-15',
+ ),
+ );
+ set_site_transient( 'update_plugins', $transient );
+ update_option( 'active_plugins', array( 'sec-plugin/sec-plugin.php' ) );
+
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'critical', $res['status'] );
+ $this->assertStringContainsString( 'security issues', $res['label'] );
+ $this->assertStringContainsString( 'action=deactivate', $res['description'] );
+
+ // Case 3: Active plugin closed for non-security -> recommended status.
+ $transient = new stdClass();
+ $transient->response = array(
+ 'closed-plugin/closed-plugin.php' => (object) array(
+ 'slug' => 'closed-plugin',
+ 'closed' => true,
+ 'is_security' => false,
+ ),
+ );
+ set_site_transient( 'update_plugins', $transient );
+ update_option( 'active_plugins', array( 'closed-plugin/closed-plugin.php' ) );
+
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'recommended', $res['status'] );
+ $this->assertStringContainsString( 'no longer maintained or available', $res['label'] );
+
+ // Case 4: Active plugin is outdated -> recommended status.
+ $transient = new stdClass();
+ $transient->response = array();
+ $transient->no_update = array(
+ 'old-plugin/old-plugin.php' => (object) array(
+ 'slug' => 'old-plugin',
+ 'is_outdated' => true,
+ ),
+ );
+ set_site_transient( 'update_plugins', $transient );
+ update_option( 'active_plugins', array( 'old-plugin/old-plugin.php' ) );
+
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'recommended', $res['status'] );
+ }
+}
diff --git a/tests/phpunit/tests/theme/closedThemes.php b/tests/phpunit/tests/theme/closedThemes.php
new file mode 100644
index 0000000000000..aa95f9151f1d7
--- /dev/null
+++ b/tests/phpunit/tests/theme/closedThemes.php
@@ -0,0 +1,285 @@
+ array(
+ $current_theme => array(
+ 'theme' => $current_theme,
+ 'new_version' => '99.0.0',
+ 'url' => 'https://wordpress.org/themes/' . $current_theme . '/',
+ 'package' => '',
+ 'closed' => true,
+ 'is_suspended' => true,
+ 'is_security' => true,
+ 'closed_date' => '2025-01-15',
+ 'reason' => 'security-issue',
+ 'reason_text' => 'Security Issue',
+ ),
+ ),
+ 'translations' => array(),
+ 'no_update' => array(
+ 'outdated-theme' => array(
+ 'theme' => 'outdated-theme',
+ 'new_version' => '1.0.0',
+ 'url' => 'https://wordpress.org/themes/outdated-theme/',
+ 'is_outdated' => true,
+ 'outdated_notice' => 'Theme has not been updated in over 2 years.',
+ ),
+ ),
+ 'closed' => array(
+ 'suspended-theme' => array(
+ 'theme' => 'suspended-theme',
+ 'status' => 'suspend',
+ 'closed' => true,
+ 'is_suspended' => true,
+ 'closed_date' => '2024-03-01',
+ 'reason_text' => 'Author request',
+ ),
+ ),
+ );
+
+ return array(
+ 'headers' => array(),
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'body' => wp_json_encode( $mock_body ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ delete_site_transient( 'update_themes' );
+ wp_update_themes( array( 'core' => 'test' ) );
+
+ $transient = get_site_transient( 'update_themes' );
+ $this->assertIsObject( $transient, 'Transient should be an object.' );
+ $this->assertArrayHasKey( $current_theme, $transient->response );
+
+ $sec_item = $transient->response[ $current_theme ];
+ $this->assertTrue( $sec_item['closed'] );
+ $this->assertTrue( $sec_item['is_suspended'] );
+ $this->assertTrue( $sec_item['is_security'] );
+ $this->assertSame( '2025-01-15', $sec_item['closed_date'] );
+ $this->assertSame( 'security-issue', $sec_item['reason'] );
+
+ $this->assertArrayHasKey( 'outdated-theme', $transient->no_update );
+ $outdated_item = $transient->no_update['outdated-theme'];
+ $this->assertTrue( $outdated_item['is_outdated'] );
+ $this->assertSame( 'Theme has not been updated in over 2 years.', $outdated_item['outdated_notice'] );
+
+ $this->assertArrayHasKey( 'suspended-theme', $transient->no_update );
+ $suspended_item = $transient->no_update['suspended-theme'];
+ $this->assertTrue( $suspended_item['closed'] );
+ $this->assertTrue( $suspended_item['is_suspended'] );
+ $this->assertSame( '2024-03-01', $suspended_item['closed_date'] );
+ }
+
+ /**
+ * Tests that wp_prepare_themes_for_js includes closure, suspension, and outdated metadata.
+ *
+ * @covers ::wp_prepare_themes_for_js
+ */
+ public function test_wp_prepare_themes_for_js_includes_closure_metadata() {
+ require_once ABSPATH . 'wp-admin/includes/theme.php';
+
+ $current_theme = get_stylesheet();
+
+ $transient = new stdClass();
+ $transient->response = array(
+ $current_theme => array(
+ 'theme' => $current_theme,
+ 'new_version' => '99.0.0',
+ 'closed' => true,
+ 'is_suspended' => true,
+ 'is_security' => true,
+ 'is_outdated' => true,
+ 'closed_date' => '2025-01-15',
+ 'reason_text' => 'Security Issue',
+ 'outdated_notice' => 'Custom theme outdated message',
+ ),
+ );
+ $transient->no_update = array();
+ set_site_transient( 'update_themes', $transient );
+
+ $prepared = wp_prepare_themes_for_js();
+ $this->assertIsArray( $prepared );
+
+ $found_theme = null;
+ foreach ( $prepared as $item ) {
+ if ( $item['id'] === $current_theme ) {
+ $found_theme = $item;
+ break;
+ }
+ }
+
+ $this->assertNotNull( $found_theme, 'Active theme must be in prepared themes.' );
+ $this->assertTrue( $found_theme['closed'] );
+ $this->assertTrue( $found_theme['is_suspended'] );
+ $this->assertTrue( $found_theme['is_security'] );
+ $this->assertTrue( $found_theme['is_outdated'] );
+ $this->assertSame( '2025-01-15', $found_theme['closedDate'] );
+ $this->assertSame( 'Security Issue', $found_theme['closedReason'] );
+ $this->assertSame( 'Custom theme outdated message', $found_theme['outdatedNotice'] );
+ }
+
+ /**
+ * Tests that themes_api handles error => closed without converting to WP_Error.
+ *
+ * @covers ::themes_api
+ */
+ public function test_themes_api_preserves_closed_theme_object() {
+ require_once ABSPATH . 'wp-admin/includes/theme.php';
+
+ add_filter(
+ 'pre_http_request',
+ static function ( $response, $parsed_args, $url ) {
+ if ( false === strpos( $url, 'api.wordpress.org/themes/info' ) ) {
+ return $response;
+ }
+
+ return array(
+ 'headers' => array(),
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'body' => wp_json_encode(
+ array(
+ 'error' => 'closed',
+ 'name' => 'Test Closed Theme',
+ 'slug' => 'test-closed-theme',
+ 'description' => 'This theme has been closed.',
+ 'status' => 'suspend',
+ 'closed' => true,
+ 'is_closed' => true,
+ 'is_suspended' => true,
+ 'is_security' => true,
+ 'closed_date' => '2025-01-15',
+ 'reason' => 'security-issue',
+ 'reason_text' => 'Security Issue',
+ 'is_outdated' => false,
+ )
+ ),
+ 'cookies' => array(),
+ 'filename' => null,
+ );
+ },
+ 10,
+ 3
+ );
+
+ $result = themes_api( 'theme_information', array( 'slug' => 'test-closed-theme' ) );
+
+ $this->assertNotWPError( $result );
+ $this->assertIsObject( $result );
+ $this->assertSame( 'closed', $result->error );
+ $this->assertTrue( $result->closed );
+ $this->assertTrue( $result->is_suspended );
+ $this->assertTrue( $result->is_security );
+ $this->assertSame( 'security-issue', $result->reason );
+ }
+
+ /**
+ * Tests Site Health check for closed and outdated themes.
+ *
+ * @covers WP_Site_Health::get_test_closed_plugins_and_themes
+ */
+ public function test_site_health_closed_themes_check() {
+ require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
+
+ $site_health = new WP_Site_Health();
+ $current_theme = get_stylesheet();
+
+ // Case 1: Active theme is normal -> good status.
+ delete_site_transient( 'update_themes' );
+ delete_site_transient( 'update_plugins' );
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'good', $res['status'] );
+
+ // Case 2: Active theme closed for security -> critical status.
+ $transient = new stdClass();
+ $transient->response = array(
+ $current_theme => array(
+ 'theme' => $current_theme,
+ 'closed' => true,
+ 'is_security' => true,
+ 'closed_date' => '2025-01-15',
+ ),
+ );
+ $transient->no_update = array();
+ set_site_transient( 'update_themes', $transient );
+
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'critical', $res['status'] );
+ $this->assertStringContainsString( 'security issues', $res['label'] );
+
+ // Case 3: Active theme suspended (non-security) -> recommended status.
+ $transient->response = array(
+ $current_theme => array(
+ 'theme' => $current_theme,
+ 'status' => 'suspend',
+ 'closed' => true,
+ 'is_suspended' => true,
+ 'is_security' => false,
+ ),
+ );
+ set_site_transient( 'update_themes', $transient );
+
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'recommended', $res['status'] );
+ $this->assertStringContainsString( 'no longer maintained or available', $res['label'] );
+
+ // Case 4: Active theme outdated -> recommended status.
+ $transient->response = array();
+ $transient->no_update = array(
+ $current_theme => array(
+ 'theme' => $current_theme,
+ 'is_outdated' => true,
+ ),
+ );
+ set_site_transient( 'update_themes', $transient );
+
+ $res = $site_health->get_test_closed_plugins_and_themes();
+ $this->assertSame( 'recommended', $res['status'] );
+ }
+}
From 85cb10e52fe8e721c66c38d616bcaf1e3b92ff69 Mon Sep 17 00:00:00 2001
From: "Mr.Alidoosti"
Date: Sun, 4 Oct 2026 14:49:39 +0330
Subject: [PATCH 2/2] test: add set_up method and update plugin test fixtures
in closed plugins tests
---
tests/phpunit/tests/plugins/closedPlugins.php | 49 +++++++++++++------
1 file changed, 34 insertions(+), 15 deletions(-)
diff --git a/tests/phpunit/tests/plugins/closedPlugins.php b/tests/phpunit/tests/plugins/closedPlugins.php
index b98b3c6677ce9..e0d04521e2a4b 100644
--- a/tests/phpunit/tests/plugins/closedPlugins.php
+++ b/tests/phpunit/tests/plugins/closedPlugins.php
@@ -8,6 +8,26 @@
*/
class Tests_Plugins_ClosedPlugins extends WP_UnitTestCase {
+ /**
+ * Sets up the test fixture.
+ */
+ public function set_up() {
+ parent::set_up();
+
+ require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
+ require_once ABSPATH . 'wp-admin/includes/screen.php';
+ require_once ABSPATH . 'wp-admin/includes/template.php';
+
+ $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ if ( is_multisite() ) {
+ grant_super_admin( $admin_id );
+ set_current_screen( 'plugins-network' );
+ } else {
+ set_current_screen( 'plugins.php' );
+ }
+ wp_set_current_user( $admin_id );
+ }
+
/**
* Cleans up options and transients after each test.
*/
@@ -17,6 +37,7 @@ public function tear_down() {
wp_cache_delete( 'plugins', 'plugins' );
remove_all_filters( 'pre_http_request' );
remove_all_filters( 'all_plugins' );
+ set_current_screen( 'front' );
parent::tear_down();
}
@@ -285,38 +306,36 @@ public function test_wp_plugins_list_table_badges() {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-plugins-list-table.php';
- set_current_screen( 'plugins.php' );
-
$transient = new stdClass();
$transient->response = array(
- 'sec/sec.php' => (object) array(
- 'slug' => 'sec',
+ 'hello.php' => (object) array(
+ 'slug' => 'hello',
'closed' => true,
'is_security' => true,
),
);
$transient->no_update = array(
- 'gen/gen.php' => (object) array(
- 'slug' => 'gen',
+ 'internationalized-plugin.php' => (object) array(
+ 'slug' => 'internationalized-plugin',
'closed' => true,
'is_security' => false,
),
- 'old/old.php' => (object) array(
- 'slug' => 'old',
+ 'custom-internationalized-plugin/custom-internationalized-plugin.php' => (object) array(
+ 'slug' => 'custom-internationalized-plugin',
'is_outdated' => true,
),
);
set_site_transient( 'update_plugins', $transient );
- $table = new WP_Plugins_List_Table();
+ $table = new WP_Plugins_List_Table( array( 'screen' => get_current_screen() ) );
// Security closed plugin.
ob_start();
$table->single_row(
array(
- 'sec/sec.php',
+ 'hello.php',
array(
- 'Name' => 'Security Plugin',
+ 'Name' => 'Hello Dolly',
'Version' => '1.0.0',
'Description' => 'Test',
),
@@ -330,9 +349,9 @@ public function test_wp_plugins_list_table_badges() {
ob_start();
$table->single_row(
array(
- 'gen/gen.php',
+ 'internationalized-plugin.php',
array(
- 'Name' => 'General Plugin',
+ 'Name' => 'Internationalized Plugin',
'Version' => '1.0.0',
'Description' => 'Test',
),
@@ -346,9 +365,9 @@ public function test_wp_plugins_list_table_badges() {
ob_start();
$table->single_row(
array(
- 'old/old.php',
+ 'custom-internationalized-plugin/custom-internationalized-plugin.php',
array(
- 'Name' => 'Old Plugin',
+ 'Name' => 'Custom Internationalized Plugin',
'Version' => '1.0.0',
'Description' => 'Test',
),
|