v1.9.1 版本发布

This commit is contained in:
文派备案 2025-04-23 11:51:16 +08:00
parent 00d6c47799
commit 308c0faebd
10 changed files with 1932 additions and 0 deletions

496
class-plugin-report.php Normal file
View file

@ -0,0 +1,496 @@
<?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);
}
}
}
}

85
css/plugin-report.css Normal file
View file

@ -0,0 +1,85 @@
/* risk indicator classes */
td.pr-risk-low {
background-color: rgba( 0, 255, 0, 0.075 );
color: #090 !important;
font-weight: bold;
}
td.pr-risk-high {
background-color: rgba( 255, 0, 0, 0.075 );
color: #c00 !important;
font-weight: bold;
}
td.pr-risk-medium {
font-weight: bold;
}
/* additional info in table cells */
span.pr-additional-info {
font-weight: normal;
color: #555;
}
/* progress bar */
#plugin-report-progress progress {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 100%;
height: 1em;
border: 1px solid #e5e5e5;
background-color: #fff;
color: #3858e9;
}
#plugin-report-progress progress::-webkit-progress-bar {
background-color: #fff;
}
#plugin-report-progress progress::-webkit-progress-value {
background-color: #3858e9;
}
#plugin-report-progress progress::-moz-progress-bar {
background-color: #3858e9;
}
/* styling for the tablesort script */
th[role=columnheader]:not(.no-sort) {
cursor: pointer;
}
th[role=columnheader]:not(.no-sort):after {
content: '';
float: right;
margin-top: 7px;
border-width: 0 4px 4px;
border-style: solid;
border-color: #404040 transparent;
visibility: hidden;
opacity: 0;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
th[aria-sort=ascending]:not(.no-sort):after {
border-bottom: none;
border-width: 4px 4px 0;
}
th[aria-sort]:not(.no-sort):after {
visibility: visible;
opacity: 0.4;
}
th[role=columnheader]:not(.no-sort):hover:after {
visibility: visible;
opacity: 1;
}

120
js/plugin-report.js Normal file
View file

@ -0,0 +1,120 @@
jQuery(document).ready( function( $ ){
var rtpr_slugs = plugin_report_vars.plugin_slugs;
var rtpr_slugs_array = rtpr_slugs.split(',');
var rtpr_nrof_plugins = rtpr_slugs_array.length;
var rtpr_progress = 0;
function rtpr_process_next_plugin(){
if( rtpr_slugs_array.length > 0 ){
var slug = rtpr_slugs_array.shift();
if( $( '.plugin-report-row-temp-' + slug ).length ){
rtpr_get_plugin_info( slug );
} else {
rtpr_process_next_plugin();
}
}
// update the progress information on the page
var perc = Math.ceil( ( rtpr_progress / rtpr_nrof_plugins ) * 100 );
if( perc < 100 ){
if( ! $( '#plugin-report-progress' ).find( 'progress' ).length ){
$( '#plugin-report-progress' ).html( '<progress max="100" value="0"></progress>' );
}
$( '#plugin-report-progress progress' ).prop( 'value', perc );
rtpr_progress++;
} else {
// Remove the progress bar.
$('#plugin-report-progress').html( '' );
// initialize sorting on table
new Tablesort(document.getElementById('plugin-report-table'));
// Create the export button.
$('#plugin-report-buttons').append('<button class="button" href="#" id="plugin-report-export-btn">' + plugin_report_vars.export_btn + '</button>');
// Export button event handler.
$('#plugin-report-export-btn').click( function( e ){
// Call the function that does the exporting.
rtpr_export_table();
});
}
}
function rtpr_get_plugin_info( slug ){
var data = {
'action': 'rt_get_plugin_info',
'slug': slug,
'nonce': plugin_report_vars.ajax_nonce
};
jQuery.post( ajaxurl, data, function(response) {
// parse the response
obj = jQuery.parseJSON(response);
// replace the temporary table row with the new data
$('#plugin-report-table .plugin-report-row-temp-' + slug ).replaceWith( obj.html );
// on to the next...
rtpr_process_next_plugin();
});
}
// kick things off
rtpr_process_next_plugin();
// Export CSV file.
function rtpr_export_table(){
var csv_data = '';
var counter = 0;
// Loop trough the table header to add the header cells.
$('#plugin-report-table thead tr').each(function(){
// Use a column counter, because we'll need ot insert two extra columns.
counter = 0;
// Loop through the header cells.
$(this).find('th').each(function(){
// Remove any comma's from the cell contents, then add to output.
csv_data += $(this).text().replace(/,/g, ".") + ',';
// If this is the first column, add the plugin url column
if( counter == 0 ){
csv_data += plugin_report_vars.plugin_url_header + ',';
}
// If this is the second column, add the author url column
if( counter == 1 ){
csv_data += plugin_report_vars.author_url_header + ',';
}
counter++;
});
// End of the line.
csv_data += "\n";
});
// Loop through the regular rows to get their data
$('#plugin-report-table tbody tr').each(function(){
// Use a column counter to insert the two columns.
counter = 0;
// Loop through all regular cells.
$(this).find('td').each(function(){
// Remove any comma's from the cell contents, then add to output.
csv_data += $(this).text().replace(/,/g, ".") + ',';
// If this is one of the first two columns, add a url column.
if( counter <2 ){
var href = '';
$(this).find('a').each(function(){
// Get the href attribute, but strip any url vars to keep it short.
href = $(this).attr('href').split('#')[0].split('?')[0];
});
// Add to the output.
csv_data += href + ',';
}
counter++;
});
// End of the line.
csv_data += "\n";
});
// Create a link element, clickit and remove it.
var link = document.createElement( 'a' );
var now = new Date();
link.download = 'plugin-report-' + now.getFullYear() + '-' + String( '0' + (now.getMonth()+1) ).slice(-2) + '-' + String( '0' + now.getDate() ).slice(-2) + '.csv';
link.href = URL.createObjectURL( new Blob( [ "\ufeff", csv_data ], {type: 'text/csv; header=present'} ) );
link.click();
link.remove();
}
});

6
js/tablesort.dotsep.min.js vendored Normal file
View file

@ -0,0 +1,6 @@
/*!
* tablesort v5.2.1 (2021-10-30)
* http://tristen.ca/tablesort/demo/
* Copyright (c) 2021 ; Licensed MIT
*/
Tablesort.extend("dotsep",function(a){return/^(\d+\.)+\d+$/.test(a)},function(a,b){a=a.split("."),b=b.split(".");for(var c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(a[e],10),d=parseInt(b[e],10),c!==d){if(c>d)return-1;if(c<d)return 1}return 0});

6
js/tablesort.min.js vendored Normal file
View file

@ -0,0 +1,6 @@
/*!
* tablesort v5.2.1 (2021-10-30)
* http://tristen.ca/tablesort/demo/
* Copyright (c) 2021 ; Licensed MIT
*/
!function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a,b){return a.getAttribute(b.sortAttribute||"data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a<b?1:-1},f=function(a,b){return[].slice.call(a).find(function(a){return a.getAttribute("data-sort-column-key")===b})},g=function(a,b){return function(c,d){var e=a(c.td,d.td);return 0===e?b?d.index-c.index:c.index-d.index:e}};a.extend=function(a,c,d){if("function"!=typeof c||"function"!=typeof d)throw new Error("Pattern and sort must be a function");b.push({name:a,pattern:c,sort:d})},a.prototype={init:function(a,b){var c,d,e,f,g=this;if(g.table=a,g.thead=!1,g.options=b,a.rows&&a.rows.length>0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e<a.tHead.rows.length;e++)if("thead"===a.tHead.rows[e].getAttribute("data-sort-method")){c=a.tHead.rows[e];break}c||(c=a.tHead.rows[a.tHead.rows.length-1]),g.thead=!0}else c=a.rows[0];if(c){var h=function(){g.current&&g.current!==this&&g.current.removeAttribute("aria-sort"),g.current=this,g.sortTable(this)};for(e=0;e<c.cells.length;e++)f=c.cells[e],f.setAttribute("role","columnheader"),"none"!==f.getAttribute("data-sort-method")&&(f.tabindex=0,f.addEventListener("click",h,!1),null!==f.getAttribute("data-sort-default")&&(d=f));d&&(g.current=d,g.sortTable(d))}},sortTable:function(a,h){var i=this,j=a.getAttribute("data-sort-column-key"),k=a.cellIndex,l=e,m="",n=[],o=i.thead?0:1,p=a.getAttribute("data-sort-method"),q=a.getAttribute("aria-sort");if(i.table.dispatchEvent(c("beforeSort")),h||(q="ascending"===q?"descending":"descending"===q?"ascending":i.options.descending?"descending":"ascending",a.setAttribute("aria-sort",q)),!(i.table.rows.length<2)){if(!p){for(var r;n.length<3&&o<i.table.tBodies[0].rows.length;)r=j?f(i.table.tBodies[0].rows[o].cells,j):i.table.tBodies[0].rows[o].cells[k],m=r?d(r,i.options):"",m=m.trim(),m.length>0&&n.push(m),o++;if(!n)return}for(o=0;o<b.length;o++)if(m=b[o],p){if(m.name===p){l=m.sort;break}}else if(n.every(m.pattern)){l=m.sort;break}for(i.col=k,o=0;o<i.table.tBodies.length;o++){var s,t=[],u={},v=0,w=0;if(!(i.table.tBodies[o].rows.length<2)){for(s=0;s<i.table.tBodies[o].rows.length;s++){var r;m=i.table.tBodies[o].rows[s],"none"===m.getAttribute("data-sort-method")?u[v]=m:(r=j?f(m.cells,j):m.cells[i.col],t.push({tr:m,td:r?d(r,i.options):"",index:v})),v++}for("descending"===q?t.sort(g(l,!0)):(t.sort(g(l,!1)),t.reverse()),s=0;s<v;s++)u[s]?(m=u[s],w++):m=t[s-w].tr,i.table.tBodies[o].appendChild(m)}}i.table.dispatchEvent(c("afterSort"))}},refresh:function(){void 0!==this.current&&this.sortTable(this.current,!0)}},"undefined"!=typeof module&&module.exports?module.exports=a:window.Tablesort=a}();

6
js/tablesort.number.min.js vendored Normal file
View file

@ -0,0 +1,6 @@
/*!
* tablesort v5.2.1 (2021-10-30)
* http://tristen.ca/tablesort/demo/
* Copyright (c) 2021 ; Licensed MIT
*/
!function(){var a=function(a){return a.replace(/[^\-?0-9.]/g,"")},b=function(a,b){return a=parseFloat(a),b=parseFloat(b),a=isNaN(a)?0:a,b=isNaN(b)?0:b,a-b};Tablesort.extend("number",function(a){return a.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/)||a.match(/^[-+]?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/)||a.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/)},function(c,d){return c=a(c),d=a(d),b(d,c)})}();

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,469 @@
msgid ""
msgstr ""
"Project-Id-Version: Multisite Plugin Control\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-15 03:24+0000\n"
"PO-Revision-Date: 2025-04-15 03:39+0000\n"
"Last-Translator: \n"
"Language-Team: 简体中文\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.6.11; wp-6.7.2\n"
"X-Domain: multisite-plugin-control"
#: multisite-plugin-control.php:581
#, php-format
msgid "%s activated on %d sites."
msgstr "%s 已在 %d 个站点上启用。"
#: multisite-plugin-control.php:609
#, php-format
msgid "%s deactivated on %d sites."
msgstr "%s 在 %d 个站点上已停用。"
#: multisite-plugin-control.php:547
#, php-format
msgid "%s has been mass activated."
msgstr "%s 已被批量启用。"
#: multisite-plugin-control.php:556
#, php-format
msgid "%s has been mass deactivated."
msgstr "%s 已被批量停用。"
#: class-plugin-report.php:332
#, php-format
msgid "(%1$s available, requires PHP %2$s)"
msgstr "%1$s 可用,需要 PHP %2$s"
#: class-plugin-report.php:328
#, php-format
msgid "(%1$s available, requires WP %2$s and PHP %3$s)"
msgstr "%1$s 可用,需要 WP %2$s 和 PHP %3$s"
#: class-plugin-report.php:330
#, php-format
msgid "(%1$s available, requires WP %2$s)"
msgstr "%1$s 可用,需要 WP %2$s"
#: class-plugin-report.php:334
#, php-format
msgid "(%s available)"
msgstr "%s 可用)"
#: multisite-plugin-control.php:161
msgid "Activate All"
msgstr "全部启用"
#: multisite-plugin-control.php:236
msgid "Activate or deactivate plugins across all existing blogs."
msgstr "在所有现有博客中启用或停用插件。"
#: class-plugin-report.php:72
msgid "Activated"
msgstr "启用"
#: multisite-plugin-control.php:203
msgid "Active Sites"
msgstr "活跃站点"
#: multisite-plugin-control.php:96
msgid "All Statuses"
msgstr "所有状态"
#: multisite-plugin-control.php:98 multisite-plugin-control.php:146
msgid "All Users"
msgstr "所有用户"
#: multisite-plugin-control.php:368
msgid "An error occurred while saving."
msgstr "保存时发生错误。"
#: multisite-plugin-control.php:185
msgid "Apply"
msgstr "应用"
#: multisite-plugin-control.php:678
#, php-format
msgid "As a %s Pro Site, you now have access to all our premium plugins!"
msgstr "作为 %s 专业网站,您现在可以访问我们所有的高级插件!"
#: class-plugin-report.php:70 multisite-plugin-control.php:114
#: multisite-plugin-control.php:707
msgid "Author"
msgstr "作者"
#: class-plugin-report.php:115
msgid "Author URL"
msgstr "作者网址"
#: multisite-plugin-control.php:225
msgid "Auto Activation"
msgstr "自动启用"
#: multisite-plugin-control.php:99
msgid "Auto-Activate"
msgstr "自动启用"
#: multisite-plugin-control.php:147
msgid "Auto-Activate (All Users)"
msgstr "自动启用(所有用户)"
#: class-plugin-report.php:74
msgid "Auto-update"
msgstr "自动更新"
#: multisite-plugin-control.php:177
msgid "Bulk Actions"
msgstr "批量操作"
#: multisite-plugin-control.php:700
msgid "Checked plugins will override network settings for this site."
msgstr "已选中的插件将覆盖该站点的网络设置。"
#: multisite-plugin-control.php:230
msgid ""
"Choose who can activate/deactivate plugins: all users, Pro Sites only, or "
"none."
msgstr "选择谁可以激活/停用插件:所有用户、仅限专业网站或无。"
#: class-plugin-report.php:62
msgid "Clear cached plugin data and reload"
msgstr "清除缓存的插件数据并重新加载"
#: multisite-plugin-control.php:166
msgid "Deactivate All"
msgstr "全部停用"
#: multisite-plugin-control.php:715
msgid "Enable"
msgstr "启用"
#: multisite-plugin-control.php:232
msgid "Enable to allow all users to activate/deactivate plugins."
msgstr "启用以允许所有用户激活/停用插件。"
#: class-plugin-report.php:345
msgid "Enabled"
msgstr "已启用"
#: class-plugin-report.php:113
msgid "Export .csv file"
msgstr "导出 .csv 文件"
#: multisite-plugin-control.php:359
msgid "Failed to save settings."
msgstr "无法保存设置。"
#: multisite-plugin-control.php:583 multisite-plugin-control.php:611
msgid "Failed to select blogs."
msgstr "无法选择博客。"
#: multisite-plugin-control.php:88
msgid "Help"
msgstr "帮助"
#. Author URI of the plugin
msgid "https://wpmultisite.com"
msgstr "https://wpmultisite.com"
#. URI of the plugin
msgid "https://wpmultisite.com/plugins/multisite-plugin-control"
msgstr "https://wpmultisite.com/plugins/multisite-plugin-control"
#: class-plugin-report.php:73
msgid "Installed Version"
msgstr "安装版本"
#: multisite-plugin-control.php:512 multisite-plugin-control.php:542
#: multisite-plugin-control.php:551
msgid "Invalid nonce."
msgstr "无效的随机数。"
#: multisite-plugin-control.php:265
msgid "Last Action Time"
msgstr "最后行动时间"
#: class-plugin-report.php:75
msgid "Last Update"
msgstr "最后更新"
#: class-plugin-report.php:89
msgid "Loading..."
msgstr "加载中..."
#. Description of the plugin
msgid ""
"Manage plugin access permissions across your entire multisite network with "
"enhanced UI and statistics."
msgstr "通过增强的 UI 和统计数据管理整个多站点网络的插件访问权限。"
#: multisite-plugin-control.php:82
msgid "Manage plugin access permissions across your multisite network."
msgstr "管理多站点网络中的插件访问权限。"
#: multisite-plugin-control.php:116
msgid "Mass Activate"
msgstr "批量激活"
#: multisite-plugin-control.php:235
msgid "Mass Activation/Deactivation"
msgstr "批量激活/停用"
#: multisite-plugin-control.php:117
msgid "Mass Deactivate"
msgstr "批量停用"
#: multisite-plugin-control.php:247
msgid "Metric"
msgstr "参数"
#. Name of the plugin
#: multisite-plugin-control.php:77
msgid "Multisite Plugin Control"
msgstr "多站点插件控制"
#: class-plugin-report.php:69 multisite-plugin-control.php:112
#: multisite-plugin-control.php:705
msgid "Name"
msgstr "名称"
#: class-plugin-report.php:308
msgid "Network activated"
msgstr "网络已激活"
#: multisite-plugin-control.php:563
msgid "Network too large for mass activation."
msgstr "网络太大,无法大规模激活。"
#: multisite-plugin-control.php:591
msgid "Network too large for mass deactivation."
msgstr "网络太大,无法批量停用。"
#: multisite-plugin-control.php:487
msgid "Network too large to calculate usage stats."
msgstr "网络太大,无法计算使用情况统计数据。"
#: multisite-plugin-control.php:266
msgid "Never"
msgstr "从未"
#: class-plugin-report.php:315
msgid "No"
msgstr "否"
#: class-plugin-report.php:366 class-plugin-report.php:382
msgid "No data available"
msgstr "无可用数据"
#: class-plugin-report.php:156 class-plugin-report.php:264
msgid "No plugin data available."
msgstr "没有可用的插件数据。"
#: multisite-plugin-control.php:97 multisite-plugin-control.php:145
msgid "None"
msgstr "无选项"
#: class-plugin-report.php:347
msgid "Not enabled"
msgstr "未启用"
#: class-plugin-report.php:298
msgid "Only available in WP 5.8+"
msgstr "仅适用于 WP 5.8+"
#: multisite-plugin-control.php:408
msgid "Operation failed."
msgstr "操作失败。"
#: multisite-plugin-control.php:513 multisite-plugin-control.php:543
#: multisite-plugin-control.php:552
msgid "Permission denied."
msgstr "无权限。"
#: class-plugin-report.php:302
msgid "Please clear cache to update"
msgstr "请清除缓存以进行更新"
#: multisite-plugin-control.php:308
msgid "Please select at least one plugin."
msgstr "请至少选择一个插件。"
#: multisite-plugin-control.php:61 multisite-plugin-control.php:62
msgid "Plugin Control"
msgstr "插件控制"
#: multisite-plugin-control.php:80
msgid "Plugin Dashboard"
msgstr "插件仪表板"
#: multisite-plugin-control.php:85
msgid "Plugin Management"
msgstr "插件管理"
#: multisite-plugin-control.php:202
msgid "Plugin Name"
msgstr "插件名称"
#: multisite-plugin-control.php:699
msgid "Plugin Override Options"
msgstr "插件覆盖选项"
#: class-plugin-report.php:48 multisite-plugin-control.php:87
msgid "Plugin Report"
msgstr "插件报告"
#: class-plugin-report.php:114
msgid "Plugin URL"
msgstr "插件网址"
#: multisite-plugin-control.php:86
msgid "Plugin Usage"
msgstr "插件用量"
#: multisite-plugin-control.php:101 multisite-plugin-control.php:150
msgid "Pro Sites"
msgstr "专业网站"
#: multisite-plugin-control.php:664
msgid "Pro Sites Only"
msgstr "仅限专业网站"
#: multisite-plugin-control.php:385
msgid "Processing..."
msgstr "加载中..."
#: class-plugin-report.php:77
msgid "Rating"
msgstr "评级"
#: class-plugin-report.php:71
msgid "Repository"
msgstr "存储库"
#: class-plugin-report.php:342
msgid "Requires WordPress 5.5 or higher"
msgstr "需要 WordPress 5.5 或更高版本"
#: class-plugin-report.php:53
#, php-format
msgid "Running WordPress %1$s and PHP %2$s."
msgstr "运行 WordPress %1$s 和 PHP %2$s。"
#: multisite-plugin-control.php:186
msgid "Save Settings"
msgstr "保存设置"
#: multisite-plugin-control.php:332
msgid "Saving..."
msgstr "保存..."
#: multisite-plugin-control.php:94
msgid "Search plugins by name..."
msgstr "按名称搜索插件..."
#: multisite-plugin-control.php:417
msgid "Server error occurred."
msgstr "发生服务器错误。"
#: multisite-plugin-control.php:179
msgid "Set to All Users"
msgstr "设置为所有用户"
#: multisite-plugin-control.php:180
msgid "Set to Auto-Activate"
msgstr "设置为自动激活"
#: multisite-plugin-control.php:178
msgid "Set to None"
msgstr "设置为无"
#: multisite-plugin-control.php:182
msgid "Set to Pro Sites"
msgstr "设置为专业网站"
#: multisite-plugin-control.php:347 multisite-plugin-control.php:538
msgid "Settings saved successfully!"
msgstr "设置保存成功!"
#: multisite-plugin-control.php:242
msgid "Statistics"
msgstr "统计数据"
#: class-plugin-report.php:76
msgid "Tested WP Version"
msgstr "已测试WP版本"
#: multisite-plugin-control.php:257
msgid "Total Activations"
msgstr "总启用量"
#: multisite-plugin-control.php:261
msgid "Total Deactivations"
msgstr "总停用量"
#: multisite-plugin-control.php:253
msgid "Total Sites"
msgstr "网站总数"
#: class-plugin-report.php:294
msgid "Updates disabled"
msgstr "已禁用更新"
#: class-plugin-report.php:55
#, php-format
msgid "Upgrade to %s available"
msgstr "可升级至 %s"
#: multisite-plugin-control.php:115 multisite-plugin-control.php:227
#: multisite-plugin-control.php:704
msgid "User Control"
msgstr "用户控制"
#: multisite-plugin-control.php:248
msgid "Value"
msgstr "数值"
#: multisite-plugin-control.php:113 multisite-plugin-control.php:706
msgid "Version"
msgstr "版本"
#: class-plugin-report.php:49
msgid "View detailed information about installed plugins across the network."
msgstr "查看整个网络上已安装插件的详细信息。"
#: multisite-plugin-control.php:243
msgid "View plugin control activity across the network."
msgstr "查看整个网络上的插件控制活动。"
#: multisite-plugin-control.php:192
msgid "View plugin usage across the network."
msgstr "查看整个网络上的插件使用情况。"
#: multisite-plugin-control.php:226
msgid "When enabled, new blogs will have the plugin activated automatically."
msgstr "启用后,新博客将自动激活该插件。"
#: class-plugin-report.php:283
msgid "wp.org/wenpai.org, plugin closed"
msgstr "wp.org/wenpai.org插件已关闭"
#: class-plugin-report.php:285
msgid "wp.org/wenpai.org, plugin not found"
msgstr "wp.org/wenpai.org插件未找到"
#. Author of the plugin
msgid "WPMultisite.com"
msgstr "文派多站点"
#: class-plugin-report.php:315
msgid "Yes"
msgstr "是"
#: class-plugin-report.php:30 multisite-plugin-control.php:71
msgid "You do not have sufficient permissions to access this page."
msgstr "您没有足够的权限访问此页面。"

View file

@ -0,0 +1,742 @@
<?php
/*
* Plugin Name: Multisite Plugin Control
* Plugin URI: https://wpmultisite.com/plugins/multisite-plugin-control
* Description: Manage plugin access permissions across your entire WordPress multisite network with enhanced UI and statistics.
* Version: 1.9.1
* Author: WPMultisite.com
* Author URI: https://wpmultisite.com
* Network: true
* Requires at least: 6.7.2
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: multisite-plugin-control
* Domain Path: /languages
*/
if (!defined('WPINC')) {
die;
}
define('MULTISITE_PLUGIN_CONTROL_VERSION', '1.9.1');
define('MULTISITE_PLUGIN_CONTROL_PATH', plugin_dir_path(__FILE__));
define('MULTISITE_PLUGIN_CONTROL_URL', plugin_dir_url(__FILE__));
require_once plugin_dir_path(__FILE__) . 'class-plugin-report.php';
class MultisitePluginControl {
private $stats;
private $plugin_report;
public function __construct() {
$this->stats = get_site_option('mpc_stats', [
'total_sites' => 0,
'total_activations' => 0,
'total_deactivations' => 0,
'last_action_time' => null
]);
$this->plugin_report = new RT_Plugin_Report($this);
$this->plugin_report->init();
add_action('network_admin_menu', [$this, 'add_menu']);
add_action('wpmu_new_blog', [$this, 'new_blog'], 50);
if (!(defined('WP_CLI') && WP_CLI)) {
add_filter('all_plugins', [$this, 'remove_plugins']);
}
add_filter('plugin_action_links', [$this, 'action_links'], 10, 4);
add_action('admin_notices', [$this, 'supporter_message']);
add_action('plugins_loaded', [$this, 'localization']);
add_action('wpmueditblogaction', [$this, 'blog_options_form']);
add_action('wpmu_update_blog_options', [$this, 'blog_options_form_process']);
add_filter('plugin_row_meta', [$this, 'remove_plugin_meta'], 10, 2);
add_action('admin_init', [$this, 'remove_plugin_update_row']);
add_action('wp_ajax_mpc_save_settings', [$this, 'ajax_save_settings']);
add_action('wp_ajax_mpc_mass_activate', [$this, 'ajax_mass_activate']);
add_action('wp_ajax_mpc_mass_deactivate', [$this, 'ajax_mass_deactivate']);
}
public function localization() {
load_plugin_textdomain('multisite-plugin-control', false, dirname(plugin_basename(__FILE__)) . '/languages/');
}
public function add_menu() {
add_submenu_page(
'plugins.php',
__('Plugin Control', 'multisite-plugin-control'),
__('Plugin Control', 'multisite-plugin-control'),
'manage_network_options',
'multisite-plugin-control',
[$this, 'admin_page']
);
}
public function admin_page() {
if (!current_user_can('manage_network_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'multisite-plugin-control'));
}
$active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'management';
?>
<div class="wrap">
<h1><?php echo esc_html__('Multisite Plugin Control', 'multisite-plugin-control'); ?>
<span style="font-size: 13px; padding-left: 10px;"><?php printf(esc_html__('Version: %s', 'multisite-plugin-control'), esc_html(MULTISITE_PLUGIN_CONTROL_VERSION)); ?></span>
<a href="https://wpmultisite.com/document/multisite-plugin-control" target="_blank" class="button button-secondary" style="margin-left: 10px;"><?php esc_html_e('Document', 'multisite-plugin-control'); ?></a>
<a href="https://wpmultisite.com/forums/" target="_blank" class="button button-secondary"><?php esc_html_e('Support', 'multisite-plugin-control'); ?></a>
</h1>
<div class="card">
<h2><?php _e('Plugin Dashboard', 'multisite-plugin-control'); ?></h2>
<span id="save-status" class="notice" style="display:none; margin-top: 10px;"></span>
<p><?php _e('Manage plugin access permissions across your multisite network.', 'multisite-plugin-control'); ?></p>
<div class="styles-sync-tabs">
<button type="button" class="styles-tab <?php echo $active_tab === 'management' ? 'active' : ''; ?>" data-tab="management"><?php _e('Plugin Management', 'multisite-plugin-control'); ?></button>
<button type="button" class="styles-tab <?php echo $active_tab === 'usage' ? 'active' : ''; ?>" data-tab="usage"><?php _e('Plugin Usage', 'multisite-plugin-control'); ?></button>
<button type="button" class="styles-tab <?php echo $active_tab === 'report' ? 'active' : ''; ?>" data-tab="report"><?php _e('Plugin Report', 'multisite-plugin-control'); ?></button>
<button type="button" class="styles-tab <?php echo $active_tab === 'help' ? 'active' : ''; ?>" data-tab="help"><?php _e('Help', 'multisite-plugin-control'); ?></button>
</div>
<div class="styles-sync-content">
<div class="tab-section" data-section="management" style="<?php echo $active_tab !== 'management' ? 'display: none;' : ''; ?>">
<div style="margin-bottom: 20px;">
<input type="text" id="plugin-search" placeholder="<?php _e('Search plugins by name...', 'multisite-plugin-control'); ?>" style="width: 300px;">
<select id="plugin-filter" style="margin-left: 10px;">
<option value="all"><?php _e('All Statuses', 'multisite-plugin-control'); ?></option>
<option value="none"><?php _e('None', 'multisite-plugin-control'); ?></option>
<option value="all"><?php _e('All Users', 'multisite-plugin-control'); ?></option>
<option value="auto"><?php _e('Auto-Activate', 'multisite-plugin-control'); ?></option>
<?php if (function_exists('is_pro_site')): ?>
<option value="supporters"><?php _e('Pro Sites', 'multisite-plugin-control'); ?></option>
<?php endif; ?>
</select>
</div>
<form id="plugin-control-form" method="post">
<?php wp_nonce_field('multisite_plugin_control_nonce', 'control_nonce'); ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><input type="checkbox" id="select-all-plugins"></th>
<th><?php _e('Name', 'multisite-plugin-control'); ?></th>
<th><?php _e('Version', 'multisite-plugin-control'); ?></th>
<th><?php _e('Author', 'multisite-plugin-control'); ?></th>
<th><?php _e('User Control', 'multisite-plugin-control'); ?></th>
<th><?php _e('Mass Activate', 'multisite-plugin-control'); ?></th>
<th><?php _e('Mass Deactivate', 'multisite-plugin-control'); ?></th>
</tr>
</thead>
<tbody>
<?php
$plugins = get_plugins();
$auto_activate = (array) get_site_option('mpc_auto_activate_list', []);
$user_control = (array) get_site_option('mpc_user_control_list', []);
$supporter_control = (array) get_site_option('mpc_supporter_control_list', []);
foreach ($plugins as $file => $p) {
if (is_network_only_plugin($file) || is_plugin_active_for_network($file)) {
continue;
}
$selected = 'none';
if (in_array($file, $user_control)) $selected = 'all';
elseif (in_array($file, $auto_activate)) $selected = 'auto';
elseif (in_array($file, $supporter_control)) $selected = 'supporters';
?>
<tr data-status="<?php echo esc_attr($selected); ?>">
<td><input type="checkbox" name="selected_plugins[]" value="<?php echo esc_attr($file); ?>" class="plugin-checkbox"></td>
<td><?php echo esc_html($p['Name']); ?></td>
<td><?php echo esc_html($p['Version']); ?></td>
<td><?php echo wp_kses_post($p['Author']); ?></td>
<td>
<select name="control[<?php echo esc_attr($file); ?>]" class="control-select">
<?php
$options = [
'none' => __('None', 'multisite-plugin-control'),
'all' => __('All Users', 'multisite-plugin-control'),
'auto' => __('Auto-Activate (All Users)', 'multisite-plugin-control'),
];
if (function_exists('is_pro_site')) {
$options['supporters'] = __('Pro Sites', 'multisite-plugin-control');
}
foreach ($options as $value => $label) {
$is_selected = ($selected === $value) ? ' selected="selected"' : '';
echo '<option value="' . esc_attr($value) . '"' . $is_selected . '>' . esc_html($label) . '</option>';
}
?>
</select>
</td>
<td>
<button type="button" class="button mass-activate" data-plugin="<?php echo esc_attr($file); ?>">
<?php _e('Activate All', 'multisite-plugin-control'); ?>
</button>
</td>
<td>
<button type="button" class="button mass-deactivate" data-plugin="<?php echo esc_attr($file); ?>">
<?php _e('Deactivate All', 'multisite-plugin-control'); ?>
</button>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<p class="submit">
<select id="bulk-action" style="margin-right: 10px;">
<option value=""><?php _e('Bulk Actions', 'multisite-plugin-control'); ?></option>
<option value="set-none"><?php _e('Set to None', 'multisite-plugin-control'); ?></option>
<option value="set-all"><?php _e('Set to All Users', 'multisite-plugin-control'); ?></option>
<option value="set-auto"><?php _e('Set to Auto-Activate', 'multisite-plugin-control'); ?></option>
<?php if (function_exists('is_pro_site')): ?>
<option value="set-supporters"><?php _e('Set to Pro Sites', 'multisite-plugin-control'); ?></option>
<?php endif; ?>
</select>
<button type="button" id="apply-bulk" class="button"><?php _e('Apply', 'multisite-plugin-control'); ?></button>
<button type="button" id="save-settings" class="button button-primary"><?php _e('Save Settings', 'multisite-plugin-control'); ?></button>
</p>
</form>
</div>
<div class="tab-section" data-section="usage" style="<?php echo $active_tab !== 'usage' ? 'display: none;' : ''; ?>">
<p><?php _e('View plugin usage across the network.', 'multisite-plugin-control'); ?></p>
<?php
$usage_stats = $this->get_plugin_usage_stats();
if (isset($usage_stats['error'])) {
echo '<p class="description">' . esc_html($usage_stats['error']) . '</p>';
} else {
?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Plugin Name', 'multisite-plugin-control'); ?></th>
<th><?php _e('Active Sites', 'multisite-plugin-control'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($usage_stats as $stats): ?>
<tr>
<td><?php echo esc_html($stats['name']); ?></td>
<td><?php echo esc_html($stats['active_sites']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
}
?>
</div>
<div class="tab-section" data-section="report" style="<?php echo $active_tab !== 'report' ? 'display: none;' : ''; ?>">
<?php $this->plugin_report->render_report(); ?>
</div>
<div class="tab-section" data-section="help" style="<?php echo $active_tab !== 'help' ? 'display: none;' : ''; ?>">
<p><strong><?php _e('Auto Activation', 'multisite-plugin-control'); ?></strong><br>
<?php _e('When enabled, new blogs will have the plugin activated automatically.', 'multisite-plugin-control'); ?></p>
<p><strong><?php _e('User Control', 'multisite-plugin-control'); ?></strong><br>
<?php
if (function_exists('is_pro_site')) {
_e('Choose who can activate/deactivate plugins: all users, Pro Sites only, or none.', 'multisite-plugin-control');
} else {
_e('Enable to allow all users to activate/deactivate plugins.', 'multisite-plugin-control');
}
?></p>
<p><strong><?php _e('Mass Activation/Deactivation', 'multisite-plugin-control'); ?></strong><br>
<?php _e('Activate or deactivate plugins across all existing blogs.', 'multisite-plugin-control'); ?></p>
</div>
</div>
</div>
<div class="card">
<h2><?php _e('Statistics', 'multisite-plugin-control'); ?></h2>
<p><?php _e('View plugin control activity across the network.', 'multisite-plugin-control'); ?></p>
<table class="wp-list-table widefat fixed">
<thead>
<tr>
<th><?php _e('Metric', 'multisite-plugin-control'); ?></th>
<th><?php _e('Value', 'multisite-plugin-control'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<th><?php _e('Total Sites', 'multisite-plugin-control'); ?></th>
<td><?php echo esc_html($this->stats['total_sites']); ?></td>
</tr>
<tr>
<th><?php _e('Total Activations', 'multisite-plugin-control'); ?></th>
<td><?php echo esc_html($this->stats['total_activations']); ?></td>
</tr>
<tr>
<th><?php _e('Total Deactivations', 'multisite-plugin-control'); ?></th>
<td><?php echo esc_html($this->stats['total_deactivations']); ?></td>
</tr>
<tr>
<th><?php _e('Last Action Time', 'multisite-plugin-control'); ?></th>
<td><?php echo esc_html($this->stats['last_action_time'] ?: __('Never', 'multisite-plugin-control')); ?></td>
</tr>
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('.styles-tab').on('click', function() {
var tab = $(this).data('tab');
window.history.pushState({}, document.title, '<?php echo admin_url('network/plugins.php?page=multisite-plugin-control'); ?>&tab=' + tab);
$('.styles-tab').removeClass('active');
$(this).addClass('active');
$('.tab-section').hide();
$('.tab-section[data-section="' + tab + '"]').show();
});
function filterPlugins() {
var search = $('#plugin-search').val().toLowerCase();
var filter = $('#plugin-filter').val();
$('.wp-list-table tbody tr').each(function() {
var name = $(this).find('td:eq(1)').text().toLowerCase();
var status = $(this).data('status');
var matchesSearch = search === '' || name.includes(search);
var matchesFilter = filter === 'all' || status === filter;
$(this).toggle(matchesSearch && matchesFilter);
});
}
$('#plugin-search').on('input', filterPlugins);
$('#plugin-filter').on('change', filterPlugins);
$('#select-all-plugins').on('change', function() {
$('.plugin-checkbox').prop('checked', $(this).prop('checked'));
});
$('#apply-bulk').on('click', function() {
var action = $('#bulk-action').val();
if (!action) return;
var selected = $('.plugin-checkbox:checked');
if (selected.length === 0) {
alert('<?php _e('Please select at least one plugin.', 'multisite-plugin-control'); ?>');
return;
}
selected.each(function() {
var plugin = $(this).val();
$('select[name="control[' + plugin + ']"]').val(action.replace('set-', ''));
});
});
$('#save-settings').on('click', function() {
var $button = $(this);
var formData = $('#plugin-control-form').serializeArray();
var settings = { control: {}, nonce: '' };
formData.forEach(function(item) {
if (item.name.startsWith('control[')) {
var plugin = item.name.replace('control[', '').replace(']', '');
settings.control[plugin] = item.value;
} else if (item.name === 'control_nonce') {
settings.nonce = item.value;
}
});
$button.prop('disabled', true);
$('#save-status').text('<?php _e('Saving...', 'multisite-plugin-control'); ?>').show();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'mpc_save_settings',
settings: settings,
_ajax_nonce: settings.nonce
},
success: function(response) {
if (response.success) {
$('#save-status')
.removeClass('notice-error')
.addClass('notice-success')
.text('<?php _e('Settings saved successfully!', 'multisite-plugin-control'); ?>')
.show()
.delay(3000)
.fadeOut();
$('.wp-list-table tbody tr').each(function() {
var plugin = $(this).find('.plugin-checkbox').val();
$(this).data('status', settings.control[plugin]);
});
} else {
$('#save-status')
.removeClass('notice-success')
.addClass('notice-error')
.text(response.data || '<?php _e('Failed to save settings.', 'multisite-plugin-control'); ?>')
.show();
}
$button.prop('disabled', false);
},
error: function() {
$('#save-status')
.removeClass('notice-success')
.addClass('notice-error')
.text('<?php _e('An error occurred while saving.', 'multisite-plugin-control'); ?>')
.show();
$button.prop('disabled', false);
}
});
});
$('.mass-activate, .mass-deactivate').on('click', function() {
var $button = $(this);
var plugin = $button.data('plugin');
var actionType = $button.hasClass('mass-activate') ? 'mass_activate' : 'mass_deactivate';
if (!confirm('<?php _e('Are you sure you want to '); ?>' + (actionType === 'mass_activate' ? '<?php _e('activate'); ?>' : '<?php _e('deactivate'); ?>') + ' <?php _e('this plugin across all sites?'); ?>')) {
return;
}
$button.prop('disabled', true);
$('#save-status').text('<?php _e('Processing...', 'multisite-plugin-control'); ?>').show();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'mpc_' + actionType,
plugin: plugin,
_ajax_nonce: '<?php echo wp_create_nonce('multisite_plugin_control_nonce'); ?>'
},
success: function(response) {
if (response.success) {
$('#save-status')
.removeClass('notice-error')
.addClass('notice-success')
.text(response.data.message)
.show()
.delay(3000)
.fadeOut();
} else {
$('#save-status')
.removeClass('notice-success')
.addClass('notice-error')
.text(response.data || '<?php _e('Operation failed.', 'multisite-plugin-control'); ?>')
.show();
}
$button.prop('disabled', false);
},
error: function() {
$('#save-status')
.removeClass('notice-success')
.addClass('notice-error')
.text('<?php _e('Server error occurred.', 'multisite-plugin-control'); ?>')
.show();
$button.prop('disabled', false);
}
});
});
});
</script>
<style>
.card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
max-width: unset;
margin-top: 20px;
padding: 20px;
}
.notice {
padding: 8px 12px;
border-radius: 3px;
}
.notice-success {
background-color: #dff0d8;
border-left: 4px solid #46b450;
}
.notice-error {
background-color: #f2dede;
border-left: 4px solid #dc3232;
}
.styles-sync-tabs {
display: flex;
flex-wrap: wrap;
gap: 5px;
border-bottom: 1px solid #c3c4c7;
margin-bottom: 20px;
}
.styles-tab {
padding: 8px 16px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
border-bottom: 2px solid transparent;
}
.styles-tab.active {
border-bottom: 2px solid #007cba;
font-weight: 600;
background: #f0f0f1;
}
.styles-tab:hover:not(.active) {
background: #f0f0f1;
border-bottom-color: #dcdcde;
}
.styles-sync-content {
flex: 1;
}
table.fixed {
table-layout: auto;
}
</style>
<?php
}
private function get_plugin_usage_stats() {
global $wpdb;
$plugins = get_plugins();
$usage_stats = [];
if (wp_is_large_network()) {
return ['error' => __('Network too large to calculate usage stats.', 'multisite-plugin-control')];
}
$blogs = $wpdb->get_col($wpdb->prepare("SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND spam = 0", $wpdb->siteid));
foreach ($plugins as $file => $p) {
if (is_network_only_plugin($file) || is_plugin_active_for_network($file)) {
continue;
}
$count = 0;
foreach ($blogs as $blog_id) {
switch_to_blog($blog_id);
if (is_plugin_active($file)) {
$count++;
}
restore_current_blog();
}
$usage_stats[$file] = [
'name' => $p['Name'],
'active_sites' => $count
];
}
return $usage_stats;
}
public function ajax_save_settings() {
check_ajax_referer('multisite_plugin_control_nonce', '_ajax_nonce') || wp_send_json_error(__('Invalid nonce.', 'multisite-plugin-control'));
if (!current_user_can('manage_network_options')) wp_send_json_error(__('Permission denied.', 'multisite-plugin-control'));
$settings = isset($_POST['settings']['control']) ? (array)$_POST['settings']['control'] : [];
$supporter_control = [];
$user_control = [];
$auto_activate = [];
foreach ($settings as $plugin => $value) {
switch ($value) {
case 'supporters':
$supporter_control[] = sanitize_text_field($plugin);
break;
case 'all':
$user_control[] = sanitize_text_field($plugin);
break;
case 'auto':
$auto_activate[] = sanitize_text_field($plugin);
break;
}
}
update_site_option('mpc_supporter_control_list', $supporter_control ?: 'EMPTY');
update_site_option('mpc_user_control_list', $user_control ?: 'EMPTY');
update_site_option('mpc_auto_activate_list', $auto_activate ?: 'EMPTY');
wp_send_json_success(['message' => __('Settings saved successfully!', 'multisite-plugin-control')]);
}
public function ajax_mass_activate() {
check_ajax_referer('multisite_plugin_control_nonce', '_ajax_nonce') || wp_send_json_error(__('Invalid nonce.', 'multisite-plugin-control'));
if (!current_user_can('manage_network_options')) wp_send_json_error(__('Permission denied.', 'multisite-plugin-control'));
$plugin = sanitize_text_field($_POST['plugin']);
$this->mass_activate($plugin);
wp_send_json_success(['message' => sprintf(__('%s has been mass activated.', 'multisite-plugin-control'), $plugin)]);
}
public function ajax_mass_deactivate() {
check_ajax_referer('multisite_plugin_control_nonce', '_ajax_nonce') || wp_send_json_error(__('Invalid nonce.', 'multisite-plugin-control'));
if (!current_user_can('manage_network_options')) wp_send_json_error(__('Permission denied.', 'multisite-plugin-control'));
$plugin = sanitize_text_field($_POST['plugin']);
$this->mass_deactivate($plugin);
wp_send_json_success(['message' => sprintf(__('%s has been mass deactivated.', 'multisite-plugin-control'), $plugin)]);
}
public function mass_activate($plugin) {
global $wpdb;
if (wp_is_large_network()) {
add_settings_error('mpc_messages', 'mass_activate_fail', __('Network too large for mass activation.', 'multisite-plugin-control'), 'error');
return false;
}
set_time_limit(120);
$blogs = $wpdb->get_col($wpdb->prepare("SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND spam = 0", $wpdb->siteid));
$count = 0;
if ($blogs) {
foreach ($blogs as $blog_id) {
switch_to_blog($blog_id);
if (!is_plugin_active($plugin)) {
activate_plugin($plugin, '', false);
$count++;
}
restore_current_blog();
}
$this->update_stats('activate', count($blogs), $count);
add_settings_error('mpc_messages', 'mass_activate_success', sprintf(__('%s activated on %d sites.', 'multisite-plugin-control'), esc_html($plugin), $count), 'success');
} else {
add_settings_error('mpc_messages', 'mass_activate_fail', __('Failed to select blogs.', 'multisite-plugin-control'), 'error');
}
}
public function mass_deactivate($plugin) {
global $wpdb;
if (wp_is_large_network()) {
add_settings_error('mpc_messages', 'mass_deactivate_fail', __('Network too large for mass deactivation.', 'multisite-plugin-control'), 'error');
return false;
}
set_time_limit(120);
$blogs = $wpdb->get_col($wpdb->prepare("SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND spam = 0", $wpdb->siteid));
$count = 0;
if ($blogs) {
foreach ($blogs as $blog_id) {
switch_to_blog($blog_id);
if (is_plugin_active($plugin)) {
deactivate_plugins($plugin, true);
$count++;
}
restore_current_blog();
}
$this->update_stats('deactivate', count($blogs), $count);
add_settings_error('mpc_messages', 'mass_deactivate_success', sprintf(__('%s deactivated on %d sites.', 'multisite-plugin-control'), esc_html($plugin), $count), 'success');
} else {
add_settings_error('mpc_messages', 'mass_deactivate_fail', __('Failed to select blogs.', 'multisite-plugin-control'), 'error');
}
}
private function update_stats($action, $total_sites, $changed) {
$this->stats['total_sites'] = $total_sites;
if ($action === 'activate') {
$this->stats['total_activations'] += $changed;
} else {
$this->stats['total_deactivations'] += $changed;
}
$this->stats['last_action_time'] = current_time('mysql');
update_site_option('mpc_stats', $this->stats);
}
public function new_blog($blog_id) {
require_once(ABSPATH . 'wp-admin/includes/plugin.php');
$auto_activate = (array) get_site_option('mpc_auto_activate_list', []);
if ($auto_activate && $auto_activate[0] !== 'EMPTY') {
switch_to_blog($blog_id);
activate_plugins($auto_activate, '', false);
restore_current_blog();
}
}
public function remove_plugins($all_plugins) {
if (is_super_admin()) return $all_plugins;
$auto_activate = (array) get_site_option('mpc_auto_activate_list', []);
$user_control = (array) get_site_option('mpc_user_control_list', []);
$supporter_control = (array) get_site_option('mpc_supporter_control_list', []);
$override_plugins = (array) get_option('mpc_plugin_override_list', []);
foreach ($all_plugins as $plugin_file => $plugin_data) {
if (!in_array($plugin_file, $user_control) && !in_array($plugin_file, $auto_activate) && !in_array($plugin_file, $supporter_control) && !in_array($plugin_file, $override_plugins)) {
unset($all_plugins[$plugin_file]);
}
}
return $all_plugins;
}
public function action_links($action_links, $plugin_file, $plugin_data, $context) {
global $psts, $blog_id;
if (is_network_admin() || is_super_admin()) return $action_links;
$auto_activate = (array) get_site_option('mpc_auto_activate_list', []);
$user_control = (array) get_site_option('mpc_user_control_list', []);
$supporter_control = (array) get_site_option('mpc_supporter_control_list', []);
$override_plugins = (array) get_option('mpc_plugin_override_list', []);
if ($context !== 'active') {
if (in_array($plugin_file, $user_control) || in_array($plugin_file, $auto_activate) || in_array($plugin_file, $override_plugins)) {
return $action_links;
} elseif (in_array($plugin_file, $supporter_control) && function_exists('is_pro_site')) {
return is_pro_site() ? $action_links : ['<a style="color:red;" href="' . esc_url($psts->checkout_url($blog_id)) . '">' . __('Pro Sites Only', 'multisite-plugin-control') . '</a>'];
}
}
return $action_links;
}
public function supporter_message() {
global $pagenow;
if (is_super_admin() || $pagenow !== 'plugins.php' || !function_exists('is_pro_site')) return;
if (!is_pro_site()) {
if (function_exists('supporter_feature_notice')) {
supporter_feature_notice();
}
} else {
echo '<div class="error" style="background-color:#F9F9F9;border:0;font-weight:bold;"><p>' . sprintf(__('As a %s Pro Site, you now have access to all our premium plugins!', 'multisite-plugin-control'), get_site_option('site_name')) . '</p></div>';
}
}
public function remove_plugin_meta($plugin_meta, $plugin_file) {
if (is_network_admin() || is_super_admin()) return $plugin_meta;
remove_all_actions("after_plugin_row_$plugin_file");
return [];
}
public function remove_plugin_update_row() {
if (!is_network_admin() && !is_super_admin()) {
remove_all_actions('after_plugin_row');
}
}
public function blog_options_form($blog_id) {
$plugins = get_plugins();
$override_plugins = (array) get_blog_option($blog_id, 'mpc_plugin_override_list', []);
?>
</table>
<h3><?php _e('Plugin Override Options', 'multisite-plugin-control'); ?></h3>
<p><?php _e('Checked plugins will override network settings for this site.', 'multisite-plugin-control'); ?></p>
<table class="widefat" style="margin:10px;width:95%;">
<thead>
<tr>
<th><?php _e('User Control', 'multisite-plugin-control'); ?></th>
<th><?php _e('Name', 'multisite-plugin-control'); ?></th>
<th><?php _e('Version', 'multisite-plugin-control'); ?></th>
<th><?php _e('Author', 'multisite-plugin-control'); ?></th>
</tr>
</thead>
<tbody>
<?php
foreach ($plugins as $file => $p) {
if (is_network_only_plugin($file) || is_plugin_active_for_network($file)) continue;
$checked = in_array($file, $override_plugins) ? 'checked="checked"' : '';
echo "<tr><td><label><input name='plugins[$file]' type='checkbox' value='1' $checked> " . __('Enable', 'multisite-plugin-control') . "</label></td><td>" . esc_html($p['Name']) . "</td><td>" . esc_html($p['Version']) . "</td><td>" . wp_kses_post($p['Author']) . "</td></tr>";
}
?>
</tbody>
</table>
<?php
}
public function blog_options_form_process() {
$override_plugins = [];
if (isset($_POST['plugins']) && is_array($_POST['plugins'])) {
foreach ($_POST['plugins'] as $plugin => $value) {
$override_plugins[] = sanitize_text_field($plugin);
}
}
update_option('mpc_plugin_override_list', $override_plugins);
}
}
new MultisitePluginControl();