mirror of
https://github.com/WPMultisite/multisite-plugin-control.git
synced 2025-08-03 05:33:22 +08:00
496 lines
No EOL
23 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|
|
}
|