multisite-plugin-control/class-plugin-report.php
2025-04-23 11:51:16 +08:00

496 lines
No EOL
23 KiB
PHP

<?php
if (!defined('ABSPATH')) {
exit;
}
class RT_Plugin_Report {
const CSS_CLASS_LOW = 'pr-risk-low';
const CSS_CLASS_MED = 'pr-risk-medium';
const CSS_CLASS_HIGH = 'pr-risk-high';
const PLUGIN_VERSION = '2.1.1';
const COLS_PER_ROW = 9;
const CACHE_LIFETIME = DAY_IN_SECONDS;
const CACHE_LIFETIME_NOREPO = WEEK_IN_SECONDS;
private $parent;
public function __construct($parent = null) {
$this->parent = $parent;
}
public function init() {
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('wp_ajax_rt_get_plugin_info', [$this, 'get_plugin_info']);
add_action('upgrader_process_complete', [$this, 'upgrade_delete_cache_items'], 10, 2);
}
public function render_report() {
if (!current_user_can('manage_network_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'multisite-plugin-control'));
}
global $wp_version;
$plugins = get_plugins();
$wp_latest = $this->check_core_updates();
if (isset($_GET['clear_cache'])) {
$new_timestamp = intval($_GET['clear_cache']);
$last_timestamp = intval(get_site_transient('plugin_report_cache_cleared'));
if (!$last_timestamp || $new_timestamp > $last_timestamp) {
$this->clear_cache();
set_site_transient('plugin_report_cache_cleared', $new_timestamp, self::CACHE_LIFETIME);
}
}
?>
<div class="tab-section" data-section="report">
<h2><?php _e('Plugin Report', 'multisite-plugin-control'); ?></h2>
<p><?php _e('View detailed information about installed plugins across the network.', 'multisite-plugin-control'); ?></p>
<p>
<?php
$version_temp = '<span class="' . $this->get_version_risk_classname($wp_version, $wp_latest) . '">' . $wp_version . '</span>';
printf(__('Running WordPress %1$s and PHP %2$s.', 'multisite-plugin-control'), $version_temp, phpversion());
if (version_compare($wp_version, $wp_latest, '<')) {
printf(' (' . __('Upgrade to %s available', 'multisite-plugin-control') . ')', $wp_latest);
}
?>
</p>
<p>
<a href="<?php echo admin_url('network/plugins.php?page=multisite-plugin-control&tab=report&clear_cache=' . current_time('timestamp')); ?>
"class="button" >
<?php _e('Clear cached plugin data and reload', 'multisite-plugin-control'); ?>
</a>
</p>
<p id="plugin-report-progress"></p>
<table id="plugin-report-table" class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th data-sort-default><?php _e('Name', 'multisite-plugin-control'); ?></th>
<th><?php _e('Author', 'multisite-plugin-control'); ?></th>
<th><?php _e('Repository', 'multisite-plugin-control'); ?></th>
<th><?php _e('Activated', 'multisite-plugin-control'); ?></th>
<th data-sort-method="none" class="no-sort"><?php _e('Installed Version', 'multisite-plugin-control'); ?></th>
<th><?php _e('Auto-update', 'multisite-plugin-control'); ?></th>
<th><?php _e('Last Update', 'multisite-plugin-control'); ?></th>
<th data-sort-method="dotsep"><?php _e('Tested WP Version', 'multisite-plugin-control'); ?></th>
<th data-sort-method="number"><?php _e('Rating', 'multisite-plugin-control'); ?></th>
</tr>
</thead>
<tbody>
<?php
foreach ($plugins as $key => $plugin) {
$slug = $this->get_plugin_slug($key);
$cache_key = $this->create_cache_key($slug);
$cache = get_site_transient($cache_key);
if ($cache) {
echo $this->render_table_row($cache);
} else {
echo '<tr class="plugin-report-row-temp-' . $slug . '"><td colspan="' . self::COLS_PER_ROW . '">' . __('Loading...', 'multisite-plugin-control') . '</td></tr>';
}
}
?>
</tbody>
</table>
<p id="plugin-report-buttons"></p>
</div>
<?php
}
public function enqueue_assets($hook) {
if ('plugins_page_multisite-plugin-control' !== $hook) {
return;
}
wp_enqueue_script('plugin-report-js', plugins_url('/js/plugin-report.js', __FILE__), array('jquery', 'plugin-report-tablesort-js'), self::PLUGIN_VERSION);
wp_enqueue_script('plugin-report-tablesort-js', plugins_url('/js/tablesort.min.js', __FILE__), array('jquery'), '5.3');
wp_enqueue_script('plugin-report-tablesort-number-js', plugins_url('/js/tablesort.number.min.js', __FILE__), array('plugin-report-tablesort-js'), '5.3');
wp_enqueue_script('plugin-report-tablesort-dotsep-js', plugins_url('/js/tablesort.dotsep.min.js', __FILE__), array('plugin-report-tablesort-js'), '5.3');
$slugs = $this->get_plugin_slugs();
$slugs_str = implode(',', $slugs);
$vars = array(
'plugin_slugs' => $slugs_str,
'ajax_nonce' => wp_create_nonce('plugin_report_nonce'),
'export_btn' => __('Export .csv file', 'multisite-plugin-control'),
'plugin_url_header' => __('Plugin URL', 'multisite-plugin-control'),
'author_url_header' => __('Author URL', 'multisite-plugin-control'),
);
wp_localize_script('plugin-report-js', 'plugin_report_vars', $vars);
wp_enqueue_style('plugin-report-css', plugin_dir_url(__FILE__) . 'css/plugin-report.css', array(), self::PLUGIN_VERSION);
}
private function get_plugin_slugs() {
$plugins = get_plugins();
$slugs = array();
foreach ($plugins as $key => $plugin) {
$slugs[] = $this->get_plugin_slug($key);
}
return $slugs;
}
private function get_plugin_slug($file) {
if (strpos($file, '/') !== false) {
$parts = explode('/', $file);
} else {
$parts = explode('.', $file);
}
return sanitize_title($parts[0]);
}
public function get_plugin_info() {
if (!check_ajax_referer('plugin_report_nonce', 'nonce', false)) {
wp_die();
}
if (!current_user_can('manage_network_options')) {
wp_die();
}
if (!function_exists('plugins_api')) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
$slug = sanitize_title($_POST['slug']);
$report = $this->assemble_plugin_report($slug);
if ($report) {
$table_row = $this->render_table_row($report);
} else {
$table_row = $this->render_error_row(__('No plugin data available.', 'multisite-plugin-control'));
}
$response = array(
'html' => $table_row,
'message' => 'Success!',
);
echo wp_json_encode($response);
wp_die();
}
private function assemble_plugin_report($slug) {
if (!empty($slug)) {
$report = array();
$cache_key = $this->create_cache_key($slug);
$cache = get_site_transient($cache_key);
$plugins = get_plugins();
$auto_updates = (array) get_site_option('auto_update_plugins', array());
if (empty($cache)) {
$report['slug'] = $slug;
foreach ($plugins as $key => $plugin) {
if ($this->get_plugin_slug($key) === $slug) {
$textdomain = $plugin['TextDomain'];
if ($textdomain) {
if (!is_textdomain_loaded($textdomain)) {
if ($plugin['DomainPath']) {
load_plugin_textdomain($textdomain, false, dirname($key) . $plugin['DomainPath']);
} else {
load_plugin_textdomain($textdomain, false, dirname($key));
}
}
} elseif ('hello.php' === basename($key)) {
$textdomain = 'default';
}
if ($textdomain) {
foreach (array('Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version') as $field) {
$plugin[$field] = translate($plugin[$field], $textdomain);
}
}
$report['local_info'] = $plugin;
$report['file_path'] = $key;
$report['auto-update'] = in_array($key, $auto_updates);
$report['local_info']['Name'] = preg_replace('/\s+/u', ' ', $report['local_info']['Name']);
break;
}
}
$args = array(
'slug' => $slug,
'fields' => array(
'description' => false,
'sections' => false,
'tags' => false,
'version' => true,
'tested' => true,
'requires' => true,
'requires_php' => true,
'compatibility' => true,
'author' => true,
),
);
$parsed_repo_url = wp_parse_url($report['local_info']['UpdateURI']);
$repo_host = isset($parsed_repo_url['host']) ? $parsed_repo_url['host'] : null;
if (empty($repo_host) || strtolower($repo_host) === 'w.org' || strtolower($repo_host) === 'wp.org') {
$returned_object = plugins_api('plugin_information', $args);
}
if (isset($returned_object)) {
if (!is_wp_error($returned_object)) {
$report['repo_info'] = maybe_unserialize($returned_object);
set_site_transient($cache_key, $report, self::CACHE_LIFETIME);
} else {
$report['repo_error_code'] = $returned_object->get_error_code();
$report['repo_error_message'] = $returned_object->get_error_message();
$report['exists_in_svn'] = $this->check_exists_in_svn($slug);
set_site_transient($cache_key, $report, self::CACHE_LIFETIME_NOREPO);
}
}
} else {
$report = $cache;
}
return $report;
} else {
return null;
}
}
private function check_exists_in_svn($slug) {
$response = wp_remote_get("http://svn.wp-plugins.org/" . $slug . "/");
if (is_wp_error($response)) {
return false;
} else {
$response_code = wp_remote_retrieve_response_code($response);
if ('200' == $response_code) {
return true;
}
}
return false;
}
private function render_table_row($report) {
global $wp_version;
$wp_latest = $this->check_core_updates();
if (null === $report) {
$html = $this->render_error_row(__('No plugin data available.', 'multisite-plugin-control'));
} else {
$html = '<tr class="plugin-report-row-' . $report['slug'] . '">';
if (isset($report['local_info']['PluginURI']) && !empty($report['local_info']['PluginURI'])) {
$html .= '<td><a href="' . $report['local_info']['PluginURI'] . '"><strong>' . $report['local_info']['Name'] . '</strong></a></td>';
} else {
$html .= '<td><strong>' . $report['local_info']['Name'] . '</strong></td>';
}
if (isset($report['local_info']['AuthorURI']) && !empty($report['local_info']['AuthorURI'])) {
$html .= '<td><a href="' . $report['local_info']['AuthorURI'] . '">' . $report['local_info']['Author'] . '</a></td>';
} else {
$html .= '<td>' . $report['local_info']['Author'] . '</td>';
}
if (isset($report['local_info']['UpdateURI'])) {
$parsed_repo_url = wp_parse_url($report['local_info']['UpdateURI']);
$repo_host = isset($parsed_repo_url['host']) ? $parsed_repo_url['host'] : $report['local_info']['UpdateURI'];
if (empty($repo_host) || strtolower($repo_host) === 'w.org' || strtolower($repo_host) === 'wordpress.org') {
if (isset($report['repo_error_code']) && $report['repo_error_code'] === 'plugins_api_failed') {
if (isset($report['exists_in_svn']) && $report['exists_in_svn'] === true) {
$html .= '<td class="' . self::CSS_CLASS_HIGH . '">' . __('wp.org/wenpai.org, plugin closed', 'multisite-plugin-control') . '</td>';
} else {
$html .= '<td class="' . self::CSS_CLASS_HIGH . '">' . __('wp.org/wenpai.org, plugin not found', 'multisite-plugin-control') . '</td>';
}
} else {
$html .= '<td class="' . self::CSS_CLASS_LOW . '">wp.org/wenpai.org</td>';
}
} else {
if ($parsed_repo_url && isset($parsed_repo_url['host'])) {
$html .= '<td class="' . self::CSS_CLASS_MED . '">' . $repo_host . '</td>';
} else {
$html .= '<td class="' . self::CSS_CLASS_MED . '">' . __('Updates disabled', 'multisite-plugin-control') . '</td>';
}
}
} else if (version_compare($wp_version, '5.8', '<')) {
$html .= $this->render_error_cell(__('Only available in WP 5.8+', 'multisite-plugin-control'));
} else {
$html .= $this->render_error_cell();
}
$active = __('Please clear cache to update', 'multisite-plugin-control');
$css_class = self::CSS_CLASS_MED;
if (is_multisite()) {
$activation_status = $this->get_multisite_activation($report['file_path']);
if (true === $activation_status['network']) {
$css_class = self::CSS_CLASS_LOW;
$html .= '<td class="' . $css_class . '">' . __('Network activated', 'multisite-plugin-control') . '</td>';
} else {
$css_class = ($activation_status['active'] > 0) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
$html .= '<td class="' . $css_class . '">' . $activation_status['active'] . '/' . $activation_status['sites'] . '</td>';
}
} else {
if (isset($report['file_path'])) {
$active = is_plugin_active($report['file_path']) ? __('Yes', 'multisite-plugin-control') : __('No', 'multisite-plugin-control');
$css_class = is_plugin_active($report['file_path']) ? self::CSS_CLASS_LOW : self::CSS_CLASS_HIGH;
}
$html .= '<td class="' . $css_class . '">' . $active . '</td>';
}
if (isset($report['repo_info'])) {
$css_class = $this->get_version_risk_classname($report['local_info']['Version'], $report['repo_info']->version);
$html .= '<td class="' . $css_class . '">';
$html .= $report['local_info']['Version'];
if ($report['local_info']['Version'] !== $report['repo_info']->version) {
$needs_php_upgrade = isset($report['repo_info']->requires_php) ? version_compare(phpversion(), $report['repo_info']->requires_php, '<') : false;
$needs_wp_upgrade = isset($report['repo_info']->requires) ? version_compare($wp_version, $report['repo_info']->requires, '<') : false;
if ($needs_wp_upgrade && $needs_php_upgrade) {
$html .= ' <span class="pr-additional-info">' . sprintf(__('(%1$s available, requires WP %2$s and PHP %3$s)', 'multisite-plugin-control'), $report['repo_info']->version, $report['repo_info']->requires, $report['repo_info']->requires_php) . '</span>';
} elseif ($needs_wp_upgrade) {
$html .= ' <span class="pr-additional-info">' . sprintf(__('(%1$s available, requires WP %2$s)', 'multisite-plugin-control'), $report['repo_info']->version, $report['repo_info']->requires) . '</span>';
} elseif ($needs_php_upgrade) {
$html .= ' <span class="pr-additional-info">' . sprintf(__('(%1$s available, requires PHP %2$s)', 'multisite-plugin-control'), $report['repo_info']->version, $report['repo_info']->requires_php) . '</span>';
} else {
$html .= ' <span class="pr-additional-info">' . sprintf(__('(%s available)', 'multisite-plugin-control'), $report['repo_info']->version) . '</span>';
}
}
$html .= '</td>';
} else {
$html .= '<td>' . $report['local_info']['Version'] . '</td>';
}
if (version_compare($wp_version, '5.5', '<')) {
$html .= '<td>' . __('Requires WordPress 5.5 or higher', 'multisite-plugin-control') . '</td>';
} else {
if (isset($report['auto-update']) && $report['auto-update']) {
$html .= '<td class="' . self::CSS_CLASS_LOW . '">' . __('Enabled', 'multisite-plugin-control') . '</td>';
} else {
$html .= '<td>' . __('Not enabled', 'multisite-plugin-control') . '</td>';
}
}
if (isset($report['repo_info']) && isset($report['repo_info']->last_updated)) {
$time_update = new DateTime($report['repo_info']->last_updated);
$time_diff = human_time_diff($time_update->getTimestamp(), current_time('timestamp'));
$css_class = $this->get_timediff_risk_classname(current_time('timestamp') - $time_update->getTimestamp());
$html .= '<td class="' . $css_class . '" data-sort="' . $time_update->getTimestamp() . '">' . $time_diff . '</td>';
} else {
$html .= $this->render_error_cell();
}
if (isset($report['repo_info']) && isset($report['repo_info']->tested)) {
$css_class = $this->get_version_risk_classname($report['repo_info']->tested, $wp_latest, true);
$html .= '<td class="' . $css_class . '">' . $report['repo_info']->tested . '</td>';
} else {
$html .= $this->render_error_cell();
}
if (isset($report['repo_info']) && isset($report['repo_info']->num_ratings) && isset($report['repo_info']->rating)) {
$css_class = (intval($report['repo_info']->num_ratings) > 0) ? $this->get_percentage_risk_classname(intval($report['repo_info']->rating)) : '';
$value_text = ((intval($report['repo_info']->num_ratings) > 0) ? $report['repo_info']->rating . '%' : __('No data available', 'multisite-plugin-control'));
$html .= '<td class="' . $css_class . '">' . $value_text . '</td>';
} else {
$html .= $this->render_error_cell();
}
$html .= '</tr>';
}
return $html;
}
private function render_error_row($message) {
return '<tr class="pluginreport-row-error"><td colspan="' . self::COLS_PER_ROW . '">' . $message . '</td></tr>';
}
private function render_error_cell($message = null) {
if (!$message) {
$message = __('No data available', 'multisite-plugin-control');
}
return '<td class="pluginreport-cell-error">' . $message . '</td>';
}
private function get_major_version($version_string) {
$parts = explode('.', $version_string);
array_splice($parts, 2);
return implode('.', $parts);
}
private function get_version_risk_classname($available, $optimal, $major_only = false) {
if ($major_only) {
$available = $this->get_major_version($available);
$optimal = $this->get_major_version($optimal);
}
if (version_compare($available, $optimal, '>=')) {
return self::CSS_CLASS_LOW;
}
return self::CSS_CLASS_HIGH;
}
private function get_percentage_risk_classname($perc) {
if ($perc < 70) {
return self::CSS_CLASS_HIGH;
}
if ($perc < 90) {
return self::CSS_CLASS_MED;
}
return self::CSS_CLASS_LOW;
}
private function get_timediff_risk_classname($time_diff) {
$days = $time_diff / (DAY_IN_SECONDS);
if ($days > 365) {
return self::CSS_CLASS_HIGH;
}
if ($days > 90) {
return self::CSS_CLASS_MED;
}
return self::CSS_CLASS_LOW;
}
private function check_core_updates() {
global $wp_version;
$update = get_preferred_from_update_core();
if (!$update || false === $update) {
return $wp_version;
}
if ('latest' === $update->response) {
return $wp_version;
}
return $update->version;
}
private function get_multisite_activation($path) {
$activation_status = array(
'network' => false,
'active' => 0,
'sites' => 1,
);
$network_plugins = get_site_option('active_sitewide_plugins', null);
if (array_key_exists($path, $network_plugins)) {
$activation_status['network'] = true;
} else {
$args = array(
'number' => 9999,
'fields' => 'ids',
);
$sites = get_sites($args);
$activation_status['sites'] = count($sites);
foreach ($sites as $site_id) {
$plugins = get_blog_option($site_id, 'active_plugins', null);
if ($plugins) {
foreach ($plugins as $plugin_path) {
if ($plugin_path === $path) {
$activation_status['active']++;
}
}
}
}
}
return $activation_status;
}
private function create_cache_key($slug) {
$slug_hash = hash('sha256', $slug);
$cache_key = 'rtpr_' . substr($slug_hash, 0, 35);
return $cache_key;
}
private function clear_cache() {
$plugins = get_plugins();
foreach ($plugins as $key => $plugin) {
$slug = $this->get_plugin_slug($key);
$this->clear_cache_item($slug);
}
}
private function clear_cache_item($slug) {
if (isset($slug)) {
$cache_key = $this->create_cache_key($slug);
delete_site_transient($cache_key);
}
}
public function upgrade_delete_cache_items($upgrader, $data) {
if (isset($data) && isset($data['plugins']) && is_array($data['plugins'])) {
foreach ($data['plugins'] as $key => $value) {
$slug = $this->get_plugin_slug($value);
$this->clear_cache_item($slug);
}
}
}
}