初版提交

This commit is contained in:
文派备案 2025-03-24 15:02:54 +08:00
parent e682403182
commit 814e65f90e
5 changed files with 1304 additions and 0 deletions

225
admin-page.php Normal file
View file

@ -0,0 +1,225 @@
<?php
function bpi_render_admin_page() {
if (!bpi_user_can_install()) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bulk-plugin-installer'));
}
wp_enqueue_style('bpi-admin-style', BPI_URL . 'css/admin.css', [], BPI_VERSION);
wp_enqueue_script('bpi-admin', BPI_URL . 'js/admin.js', ['jquery'], BPI_VERSION, true);
wp_localize_script('bpi-admin', 'bpiAjax', [
'nonce' => wp_create_nonce('bpi_installer'),
'ajaxurl' => admin_url('admin-ajax.php')
]);
?>
<div class="wrap">
<h1><?php echo esc_html__('Bulk Plugin & Theme Installer', 'bulk-plugin-installer'); ?>
<span style="font-size: 13px; padding-left: 10px;"><?php printf(esc_html__('Version: %s', 'bulk-plugin-installer'), esc_html(BPI_VERSION)); ?></span>
<a href="https://wpmultisite.com/document/" target="_blank" class="button button-secondary" style="margin-left: 10px;"><?php esc_html_e('Document', 'bulk-plugin-installer'); ?></a>
<a href="https://wpmultisite.com/forums/" target="_blank" class="button button-secondary"><?php esc_html_e('Support', 'bulk-plugin-installer'); ?></a>
</h1>
<div class="bpi-container">
<div class="bpi-card">
<div id="bpi-tabs">
<div class="bpi-tabs-nav">
<button type="button" class="bpi-tab active" data-tab="plugins"><?php _e('Plugins', 'bulk-plugin-installer'); ?></button>
<button type="button" class="bpi-tab" data-tab="themes"><?php _e('Themes', 'bulk-plugin-installer'); ?></button>
<button type="button" class="bpi-tab" data-tab="settings"><?php _e('Settings', 'bulk-plugin-installer'); ?></button>
</div>
<div id="plugins" class="bpi-tab-content active">
<h2><?php _e('Install Plugins', 'bulk-plugin-installer'); ?></h2>
<p><?php _e('Install multiple plugins from various sources.', 'bulk-plugin-installer'); ?></p>
<form id="bulk-plugin-form" class="bpi-form" enctype="multipart/form-data">
<div class="bpi-form-row">
<label for="plugin-install-type"><?php _e('Installation Source:', 'bulk-plugin-installer'); ?></label>
<select id="plugin-install-type" name="install_type" class="bpi-select">
<option value="repository"><?php _e('WordPress.org Repository', 'bulk-plugin-installer'); ?></option>
<option value="wenpai"><?php _e('WenPai.org Repository (China Mirror)', 'bulk-plugin-installer'); ?></option>
<option value="url"><?php _e('Remote URL', 'bulk-plugin-installer'); ?></option>
<option value="upload"><?php _e('Upload ZIP', 'bulk-plugin-installer'); ?></option>
</select>
</div>
<div class="bpi-form-row source-input repository-source active">
<label for="plugin-slugs"><?php _e('Plugin Slugs:', 'bulk-plugin-installer'); ?></label>
<textarea id="plugin-slugs" name="items" rows="8"
placeholder="<?php esc_attr_e('Enter plugin slugs, one per line (e.g., akismet)', 'bulk-plugin-installer'); ?>"
></textarea>
</div>
<div class="bpi-form-row source-input wenpai-source">
<label for="plugin-wenpai-slugs"><?php _e('Plugin Slugs (WenPai.org):', 'bulk-plugin-installer'); ?></label>
<textarea id="plugin-wenpai-slugs" name="items" rows="8"
placeholder="<?php esc_attr_e('Enter plugin slugs, one per line (e.g., akismet)', 'bulk-plugin-installer'); ?>"
></textarea>
</div>
<div class="bpi-form-row source-input url-source">
<label for="plugin-urls"><?php _e('Download URLs:', 'bulk-plugin-installer'); ?></label>
<textarea id="plugin-urls" name="items" rows="8"
placeholder="<?php esc_attr_e('Enter download URLs, one per line', 'bulk-plugin-installer'); ?>"
></textarea>
</div>
<div class="bpi-form-row source-input upload-source">
<label for="plugin-files"><?php _e('ZIP Files:', 'bulk-plugin-installer'); ?></label>
<div class="file-upload-container">
<input type="file" id="plugin-files" name="plugin_files[]" multiple accept=".zip" />
<div class="upload-instructions">
<?php _e('Drag and drop plugin ZIP files here or click to select files', 'bulk-plugin-installer'); ?>
</div>
<div id="selected-files" class="selected-files"></div>
</div>
</div>
<div class="bpi-form-row">
<?php wp_nonce_field('bpi_installer', 'bpi_nonce'); ?>
<button type="submit" class="button button-primary button-large">
<?php _e('Install Plugins', 'bulk-plugin-installer'); ?>
</button>
</div>
</form>
</div>
<div id="themes" class="bpi-tab-content">
<h2><?php _e('Install Themes', 'bulk-plugin-installer'); ?></h2>
<p><?php _e('Install multiple themes from various sources.', 'bulk-plugin-installer'); ?></p>
<form id="bulk-theme-form" class="bpi-form" enctype="multipart/form-data">
<div class="bpi-form-row">
<label for="theme-install-type"><?php _e('Installation Source:', 'bulk-plugin-installer'); ?></label>
<select id="theme-install-type" name="install_type" class="bpi-select">
<option value="repository"><?php _e('WordPress.org Repository', 'bulk-plugin-installer'); ?></option>
<option value="wenpai"><?php _e('WenPai.org Repository (China Mirror)', 'bulk-plugin-installer'); ?></option>
<option value="url"><?php _e('Remote URL', 'bulk-plugin-installer'); ?></option>
<option value="upload"><?php _e('Upload ZIP', 'bulk-plugin-installer'); ?></option>
</select>
</div>
<div class="bpi-form-row source-input repository-source active">
<label for="theme-slugs"><?php _e('Theme Slugs:', 'bulk-plugin-installer'); ?></label>
<textarea id="theme-slugs" name="items" rows="8"
placeholder="<?php esc_attr_e('Enter theme slugs, one per line (e.g., twentytwenty)', 'bulk-plugin-installer'); ?>"
></textarea>
</div>
<div class="bpi-form-row source-input wenpai-source">
<label for="theme-wenpai-slugs"><?php _e('Theme Slugs (WenPai.org):', 'bulk-plugin-installer'); ?></label>
<textarea id="theme-wenpai-slugs" name="items" rows="8"
placeholder="<?php esc_attr_e('Enter theme slugs, one per line (e.g., twentytwenty)', 'bulk-plugin-installer'); ?>"
></textarea>
</div>
<div class="bpi-form-row source-input url-source">
<label for="theme-urls"><?php _e('Download URLs:', 'bulk-plugin-installer'); ?></label>
<textarea id="theme-urls" name="items" rows="8"
placeholder="<?php esc_attr_e('Enter download URLs, one per line', 'bulk-plugin-installer'); ?>"
></textarea>
</div>
<div class="bpi-form-row source-input upload-source">
<label for="theme-files"><?php _e('ZIP Files:', 'bulk-plugin-installer'); ?></label>
<div class="file-upload-container">
<input type="file" id="theme-files" name="theme_files[]" multiple accept=".zip" />
<div class="upload-instructions">
<?php _e('Drag and drop theme ZIP files here or click to select files', 'bulk-plugin-installer'); ?>
</div>
<div id="selected-files" class="selected-files"></div>
</div>
</div>
<div class="bpi-form-row">
<?php wp_nonce_field('bpi_installer', 'bpi_nonce'); ?>
<button type="submit" class="button button-primary button-large">
<?php _e('Install Themes', 'bulk-plugin-installer'); ?>
</button>
</div>
</form>
</div>
<div id="settings" class="bpi-tab-content">
<h2><?php _e('Settings', 'bulk-plugin-installer'); ?></h2>
<p><?php _e('Configure plugin installation settings.', 'bulk-plugin-installer'); ?></p>
<span id="settings-status" class="notice" style="display:none;"></span>
<?php if (!current_user_can('manage_options')): ?>
<p><?php _e('You need administrator privileges to modify these settings.', 'bulk-plugin-installer'); ?></p>
<?php else: ?>
<form id="bpi-settings-form" class="bpi-form">
<?php wp_nonce_field('bpi_installer', 'bpi_nonce'); ?>
<div class="bpi-form-row">
<label><?php _e('Allowed Roles', 'bulk-plugin-installer'); ?></label>
<?php
$allowed_roles = get_option('bpi_allowed_roles', BPI_ALLOWED_ROLES);
$roles = wp_roles()->get_names();
foreach ($roles as $role => $label) {
printf(
'<label><input type="checkbox" name="bpi_allowed_roles[]" value="%s" %s> %s</label><br>',
esc_attr($role),
in_array($role, $allowed_roles) ? 'checked' : '',
esc_html($label)
);
}
?>
</div>
<div class="bpi-form-row">
<label for="bpi_custom_domains"><?php _e('Additional Trusted Domains', 'bulk-plugin-installer'); ?></label>
<textarea name="bpi_custom_domains" id="bpi_custom_domains" rows="5" class="large-text code"
placeholder="<?php esc_attr_e('Enter one root domain per line (e.g., example.com)', 'bulk-plugin-installer'); ?>"
><?php echo esc_textarea(get_option('bpi_custom_domains', '')); ?></textarea>
<p class="description">
<?php _e('Enter root domains (one per line) for remote installation. Subdomains are automatically included.', 'bulk-plugin-installer'); ?>
</p>
</div>
<div class="bpi-form-row">
<button type="submit" class="button button-primary"><?php _e('Save Settings', 'bulk-plugin-installer'); ?></button>
</div>
</form>
<?php endif; ?>
</div>
</div>
<div id="installation-results" class="bpi-results"></div>
</div>
<div class="bpi-card">
<h2><?php _e('Statistics', 'bulk-plugin-installer'); ?></h2>
<p><?php _e('View statistics of plugin and theme installations.', 'bulk-plugin-installer'); ?></p>
<?php
$stats = get_option('bpi_statistics', [
'total_installs' => 0,
'successful_installs' => 0,
'failed_installs' => 0,
'last_install_time' => ''
]);
?>
<table class="wp-list-table widefat fixed">
<thead>
<tr>
<th><?php _e('Metric', 'bulk-plugin-installer'); ?></th>
<th><?php _e('Value', 'bulk-plugin-installer'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<th><?php _e('Total Installs', 'bulk-plugin-installer'); ?></th>
<td><?php echo esc_html($stats['total_installs']); ?></td>
</tr>
<tr>
<th><?php _e('Successful Installs', 'bulk-plugin-installer'); ?></th>
<td><?php echo esc_html($stats['successful_installs']); ?></td>
</tr>
<tr>
<th><?php _e('Failed Installs', 'bulk-plugin-installer'); ?></th>
<td><?php echo esc_html($stats['failed_installs']); ?></td>
</tr>
<tr>
<th><?php _e('Last Install Time', 'bulk-plugin-installer'); ?></th>
<td><?php echo esc_html($stats['last_install_time'] ?: __('Never Installed', 'bulk-plugin-installer')); ?></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<?php
}

325
bulk-plugin-installer.php Normal file
View file

@ -0,0 +1,325 @@
<?php
/**
* Plugin Name: Bulk Plugin Installer
* Plugin URI: https://wpmultisite.com/plugins/bulk-plugin-installer/
* Description: Bulk install WordPress plugins and themes from repository, URL, or ZIP uploads.
* Version: 1.1.6
* Author: WPMultisite.com
* Author URI: https://wpmultisite.com
* Network: true
* Requires at least: 5.8
* License: GPL v2 or later
* Text Domain: bulk-plugin-installer
* Requires PHP: 7.4
* Domain Path: /languages
*/
if (!defined('WPINC')) {
die;
}
define('BPI_VERSION', '1.1.6');
define('BPI_PATH', plugin_dir_path(__FILE__));
define('BPI_URL', plugin_dir_url(__FILE__));
require_once BPI_PATH . 'class-installer.php';
require_once BPI_PATH . 'admin-page.php';
function bpi_init() {
if (is_multisite()) {
if (is_network_admin()) {
add_action('network_admin_menu', 'bpi_add_network_submenu_page');
}
} else {
add_action('admin_menu', 'bpi_add_menu_page');
}
add_action('wp_ajax_bpi_install_plugins', 'bpi_handle_install_plugins');
add_action('wp_ajax_bpi_install_themes', 'bpi_handle_install_themes');
add_action('wp_ajax_bpi_save_settings', 'bpi_handle_save_settings');
}
add_action('plugins_loaded', 'bpi_init');
function bpi_add_menu_page() {
add_plugins_page(
__('Plugin Installer', 'bulk-plugin-installer'),
__('Plugin Installer', 'bulk-plugin-installer'),
'install_plugins',
'bulk-plugin-installer',
'bpi_render_admin_page',
10
);
}
function bpi_add_network_submenu_page() {
add_submenu_page(
'plugins.php',
__('Plugin Installer', 'bulk-plugin-installer'),
__('Plugin Installer', 'bulk-plugin-installer'),
'manage_network_plugins',
'bulk-plugin-installer',
'bpi_render_admin_page'
);
}
define('BPI_ALLOWED_ROLES', ['administrator', 'super_admin']);
define('BPI_TRUSTED_DOMAINS', [
'wordpress.org',
'downloads.wordpress.org',
'github.com',
'raw.githubusercontent.com',
'wenpai.cn',
'wenpai.net',
'wenpai.org',
'downloads.wenpai.net',
'weixiaoduo.com',
'feibisi.com',
'feicode.com'
]);
function bpi_register_settings() {
register_setting('bpi_settings', 'bpi_allowed_roles', ['sanitize_callback' => 'bpi_sanitize_roles']);
register_setting('bpi_settings', 'bpi_custom_domains', ['sanitize_callback' => 'sanitize_textarea_field']);
register_setting('bpi_settings', 'bpi_statistics', ['sanitize_callback' => 'bpi_sanitize_statistics']);
}
add_action('admin_init', 'bpi_register_settings');
if (is_multisite()) {
add_action('network_admin_init', 'bpi_register_settings');
}
function bpi_sanitize_roles($roles) {
if (!is_array($roles)) {
return BPI_ALLOWED_ROLES;
}
$valid_roles = array_keys(wp_roles()->get_names());
return array_intersect($roles, $valid_roles);
}
function bpi_sanitize_statistics($stats) {
return [
'total_installs' => absint($stats['total_installs'] ?? 0),
'successful_installs' => absint($stats['successful_installs'] ?? 0),
'failed_installs' => absint($stats['failed_installs'] ?? 0),
'last_install_time' => sanitize_text_field($stats['last_install_time'] ?? '')
];
}
function bpi_user_can_install() {
if (!is_user_logged_in()) {
return false;
}
if (is_multisite() && is_network_admin()) {
return current_user_can('manage_network_plugins');
}
$allowed_roles = get_option('bpi_allowed_roles', BPI_ALLOWED_ROLES);
$user = wp_get_current_user();
return !empty(array_intersect($allowed_roles, $user->roles)) || current_user_can('manage_options');
}
function bpi_is_domain_allowed($url) {
if (empty($url)) {
return false;
}
$host = parse_url($url, PHP_URL_HOST);
if (!$host) {
return false;
}
$host = strtolower($host);
$trusted_domains = array_merge(
BPI_TRUSTED_DOMAINS,
array_filter(array_map('trim', explode("\n", get_option('bpi_custom_domains', ''))))
);
$trusted_domains = array_map('strtolower', $trusted_domains);
foreach ($trusted_domains as $domain) {
if ($host === $domain || preg_match('/\.' . preg_quote($domain, '/') . '$/', $host)) {
return true;
}
}
return false;
}
function bpi_handle_install_plugins() {
check_ajax_referer('bpi_installer', 'nonce');
if (!current_user_can('install_plugins') && !(is_multisite() && current_user_can('manage_network_plugins'))) {
wp_send_json_error(__('Insufficient permissions', 'bulk-plugin-installer'));
}
$installer = new BPI_Installer();
$type = sanitize_text_field($_POST['install_type'] ?? '');
$results = [];
try {
if ($type === 'upload') {
if (!isset($_FILES['plugin_files']) || empty($_FILES['plugin_files']['name'])) {
wp_send_json_error(__('No files uploaded', 'bulk-plugin-installer'));
}
$files = [];
if (is_array($_FILES['plugin_files']['name'])) {
$file_count = count($_FILES['plugin_files']['name']);
for ($i = 0; $i < $file_count; $i++) {
if ($_FILES['plugin_files']['error'][$i] === UPLOAD_ERR_OK) {
$files[] = [
'name' => sanitize_file_name($_FILES['plugin_files']['name'][$i]),
'type' => $_FILES['plugin_files']['type'][$i],
'tmp_name' => $_FILES['plugin_files']['tmp_name'][$i],
'error' => $_FILES['plugin_files']['error'][$i],
'size' => $_FILES['plugin_files']['size'][$i]
];
}
}
} else {
if ($_FILES['plugin_files']['error'] === UPLOAD_ERR_OK) {
$files[] = [
'name' => sanitize_file_name($_FILES['plugin_files']['name']),
'type' => $_FILES['plugin_files']['type'],
'tmp_name' => $_FILES['plugin_files']['tmp_name'],
'error' => $_FILES['plugin_files']['error'],
'size' => $_FILES['plugin_files']['size']
];
}
}
if (empty($files)) {
wp_send_json_error(__('No valid files uploaded', 'bulk-plugin-installer'));
}
$results = $installer->bpi_install_plugins($files, $type);
} else {
$items = isset($_POST['items']) ? json_decode(stripslashes($_POST['items']), true) : [];
if (!is_array($items) || empty($items)) {
wp_send_json_error(__('No items provided', 'bulk-plugin-installer'));
}
$results = $installer->bpi_install_plugins($items, $type);
}
bpi_update_statistics($results);
wp_send_json_success($results);
} catch (Exception $e) {
error_log('BPI Plugin Install Error: ' . $e->getMessage());
wp_send_json_error(__('Installation failed: ', 'bulk-plugin-installer') . $e->getMessage());
}
}
function bpi_handle_install_themes() {
check_ajax_referer('bpi_installer', 'nonce');
if (!current_user_can('install_themes') && !(is_multisite() && current_user_can('manage_network_plugins'))) {
wp_send_json_error(__('Insufficient permissions', 'bulk-plugin-installer'));
}
$installer = new BPI_Installer();
$type = sanitize_text_field($_POST['install_type'] ?? '');
$results = [];
try {
if ($type === 'upload') {
if (!isset($_FILES['theme_files']) || empty($_FILES['theme_files']['name'])) {
wp_send_json_error(__('No files uploaded', 'bulk-plugin-installer'));
}
$files = [];
if (is_array($_FILES['theme_files']['name'])) {
$file_count = count($_FILES['theme_files']['name']);
for ($i = 0; $i < $file_count; $i++) {
if ($_FILES['theme_files']['error'][$i] === UPLOAD_ERR_OK) {
$files[] = [
'name' => sanitize_file_name($_FILES['theme_files']['name'][$i]),
'type' => $_FILES['theme_files']['type'][$i],
'tmp_name' => $_FILES['theme_files']['tmp_name'][$i],
'error' => $_FILES['theme_files']['error'][$i],
'size' => $_FILES['theme_files']['size'][$i]
];
}
}
} else {
if ($_FILES['theme_files']['error'] === UPLOAD_ERR_OK) {
$files[] = [
'name' => sanitize_file_name($_FILES['theme_files']['name']),
'type' => $_FILES['theme_files']['type'],
'tmp_name' => $_FILES['theme_files']['tmp_name'],
'error' => $_FILES['theme_files']['error'],
'size' => $_FILES['theme_files']['size']
];
}
}
if (empty($files)) {
wp_send_json_error(__('No valid files uploaded', 'bulk-plugin-installer'));
}
$results = $installer->bpi_install_themes($files, $type);
} else {
$items = isset($_POST['items']) ? json_decode(stripslashes($_POST['items']), true) : [];
if (!is_array($items) || empty($items)) {
wp_send_json_error(__('No items provided', 'bulk-plugin-installer'));
}
$results = $installer->bpi_install_themes($items, $type);
}
bpi_update_statistics($results);
wp_send_json_success($results);
} catch (Exception $e) {
error_log('BPI Theme Install Error: ' . $e->getMessage());
wp_send_json_error(__('Installation failed: ', 'bulk-plugin-installer') . $e->getMessage());
}
}
function bpi_handle_save_settings() {
check_ajax_referer('bpi_installer', 'nonce');
if (!current_user_can('manage_options') && !(is_multisite() && current_user_can('manage_network_options'))) {
wp_send_json_error(__('Insufficient permissions', 'bulk-plugin-installer'));
}
$roles = isset($_POST['bpi_allowed_roles']) ? (array)$_POST['bpi_allowed_roles'] : [];
$domains = isset($_POST['bpi_custom_domains']) ? sanitize_textarea_field($_POST['bpi_custom_domains']) : '';
update_option('bpi_allowed_roles', bpi_sanitize_roles($roles));
update_option('bpi_custom_domains', $domains);
wp_send_json_success(__('Settings saved successfully!', 'bulk-plugin-installer'));
}
function bpi_update_statistics($results) {
$stats = get_option('bpi_statistics', [
'total_installs' => 0,
'successful_installs' => 0,
'failed_installs' => 0,
'last_install_time' => ''
]);
$stats['total_installs'] += count($results);
foreach ($results as $result) {
if ($result['success']) {
$stats['successful_installs']++;
} else {
$stats['failed_installs']++;
}
}
$stats['last_install_time'] = current_time('mysql');
update_option('bpi_statistics', $stats);
}
register_activation_hook(__FILE__, 'bpi_activate');
function bpi_activate() {
if (version_compare(PHP_VERSION, '7.4', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die(__('This plugin requires PHP 7.4 or higher.', 'bulk-plugin-installer'));
}
$suggested_configs = [
'upload_max_filesize' => '64M',
'post_max_size' => '64M',
'max_file_uploads' => '20',
'memory_limit' => '256M',
'max_execution_time' => '300'
];
foreach ($suggested_configs as $key => $value) {
if (ini_get($key) < $value) {
error_log("BPI Warning: $key is set to " . ini_get($key) . ", recommended: $value");
}
}
if (!get_option('bpi_statistics')) {
update_option('bpi_statistics', [
'total_installs' => 0,
'successful_installs' => 0,
'failed_installs' => 0,
'last_install_time' => ''
]);
}
}

298
class-installer.php Normal file
View file

@ -0,0 +1,298 @@
<?php
class BPI_Installer {
private $wp_filesystem;
public function __construct() {
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . '/wp-admin/includes/file.php';
if (!WP_Filesystem()) {
throw new Exception(__('Unable to initialize filesystem. Please check server permissions.', 'bulk-plugin-installer'));
}
}
$this->wp_filesystem = $wp_filesystem;
}
private function is_plugin_zip($zip_path) {
$zip = new ZipArchive();
if ($zip->open($zip_path) === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
$filename = $zip->getNameIndex($i);
if (preg_match('/\.php$/', $filename)) {
$content = $zip->getFromIndex($i);
if (preg_match('/Plugin Name:/i', $content)) {
$zip->close();
return true;
}
}
}
$zip->close();
}
return false;
}
private function is_theme_zip($zip_path) {
$zip = new ZipArchive();
if ($zip->open($zip_path) === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
if (strpos($zip->getNameIndex($i), 'style.css') !== false) {
$zip->close();
return true;
}
}
$zip->close();
}
return false;
}
public function bpi_install_plugins($items, $type) {
$valid_types = ['repository', 'wenpai', 'url', 'upload'];
if (!in_array($type, $valid_types)) {
return ['error' => __('Invalid installation type', 'bulk-plugin-installer')];
}
if (empty($items) || !is_array($items)) {
return ['error' => __('No items provided', 'bulk-plugin-installer')];
}
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$results = [];
$upgrader = new Plugin_Upgrader(new WP_Ajax_Upgrader_Skin());
$installed_plugins = get_plugins();
foreach ($items as $item) {
try {
$plugin_key = null;
if ($type === 'repository' || $type === 'wenpai') {
$item = sanitize_text_field($item);
if (!preg_match('/^[a-z0-9-]+$/', $item)) {
throw new Exception(__('Invalid plugin slug', 'bulk-plugin-installer'));
}
$plugin_key = $item . '/' . $item . '.php';
} elseif ($type === 'upload') {
$file_name = sanitize_file_name($item['name']);
$plugin_key = pathinfo($file_name, PATHINFO_FILENAME) . '/' . pathinfo($file_name, PATHINFO_FILENAME) . '.php';
}
if ($plugin_key && array_key_exists($plugin_key, $installed_plugins)) {
$results[$item] = [
'success' => true,
'message' => __('Plugin already installed, skipped', 'bulk-plugin-installer')
];
continue;
}
switch ($type) {
case 'repository':
$api = plugins_api('plugin_information', [
'slug' => $item,
'fields' => ['sections' => false]
]);
if (is_wp_error($api)) {
throw new Exception($api->get_error_message());
}
$result = $upgrader->install($api->download_link);
break;
case 'wenpai':
$response = wp_remote_get("https://api.wenpai.net/wp-json/wp/v2/plugins/{$item}");
if (is_wp_error($response)) {
throw new Exception($response->get_error_message());
}
$plugin_data = json_decode(wp_remote_retrieve_body($response), true);
$download_link = $plugin_data && !empty($plugin_data['download_link'])
? $plugin_data['download_link']
: "https://downloads.wenpai.net/plugin/{$item}.latest-stable.zip";
$result = $upgrader->install($download_link);
break;
case 'url':
$item = sanitize_text_field($item);
if (!filter_var($item, FILTER_VALIDATE_URL) || !bpi_is_domain_allowed($item)) {
throw new Exception(__('Invalid or untrusted URL', 'bulk-plugin-installer'));
}
$result = $upgrader->install($item);
break;
case 'upload':
$file = $item;
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception(__('File upload error: ', 'bulk-plugin-installer') . $file['error']);
}
$file_name = sanitize_file_name($file['name']);
if (pathinfo($file_name, PATHINFO_EXTENSION) !== 'zip') {
throw new Exception(__('Only ZIP files are allowed', 'bulk-plugin-installer'));
}
$temp_file = $file['tmp_name'];
$upload_dir = wp_upload_dir();
$dest_path = $upload_dir['path'] . '/' . $file_name;
if (!move_uploaded_file($temp_file, $dest_path)) {
throw new Exception(__('Failed to move uploaded file. Check server permissions.', 'bulk-plugin-installer'));
}
// 检查 ZIP 文件类型
if (!$this->is_plugin_zip($dest_path)) {
if ($this->is_theme_zip($dest_path)) {
unlink($dest_path);
throw new Exception(__('This appears to be a theme ZIP. Please use the Themes tab to install.', 'bulk-plugin-installer'));
}
unlink($dest_path);
throw new Exception(__('Invalid plugin ZIP file', 'bulk-plugin-installer'));
}
$result = $upgrader->install($dest_path);
if (file_exists($dest_path)) {
unlink($dest_path);
}
$item = $file_name;
break;
}
if (is_wp_error($result)) {
throw new Exception($result->get_error_message());
}
$results[$item] = [
'success' => $result === true,
'message' => $result === true ? __('Successfully installed', 'bulk-plugin-installer') : __('Installation failed', 'bulk-plugin-installer')
];
} catch (Exception $e) {
$results[$item] = [
'success' => false,
'message' => $e->getMessage()
];
}
}
return $results;
}
public function bpi_install_themes($items, $type) {
$valid_types = ['repository', 'wenpai', 'url', 'upload'];
if (!in_array($type, $valid_types)) {
return ['error' => __('Invalid installation type', 'bulk-plugin-installer')];
}
if (empty($items) || !is_array($items)) {
return ['error' => __('No items provided', 'bulk-plugin-installer')];
}
require_once ABSPATH . 'wp-admin/includes/theme-install.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
$results = [];
$upgrader = new Theme_Upgrader(new WP_Ajax_Upgrader_Skin());
$installed_themes = wp_get_themes();
foreach ($items as $item) {
try {
$theme_key = null;
if ($type === 'repository' || $type === 'wenpai') {
$item = sanitize_text_field($item);
if (!preg_match('/^[a-z0-9-]+$/', $item)) {
throw new Exception(__('Invalid theme slug', 'bulk-plugin-installer'));
}
$theme_key = $item;
} elseif ($type === 'upload') {
$file_name = sanitize_file_name($item['name']);
$theme_key = pathinfo($file_name, PATHINFO_FILENAME);
}
if ($theme_key && array_key_exists($theme_key, $installed_themes)) {
$results[$item] = [
'success' => true,
'message' => __('Theme already installed, skipped', 'bulk-plugin-installer')
];
continue;
}
switch ($type) {
case 'repository':
$api = themes_api('theme_information', [
'slug' => $item,
'fields' => ['sections' => false]
]);
if (is_wp_error($api)) {
throw new Exception($api->get_error_message());
}
$result = $upgrader->install($api->download_link);
break;
case 'wenpai':
$response = wp_remote_get("https://api.wenpai.net/wp-json/wp/v2/themes/{$item}");
if (is_wp_error($response)) {
throw new Exception($response->get_error_message());
}
$theme_data = json_decode(wp_remote_retrieve_body($response), true);
$download_link = $theme_data && !empty($theme_data['download_link'])
? $theme_data['download_link']
: "https://downloads.wenpai.net/theme/{$item}.latest-stable.zip";
$result = $upgrader->install($download_link);
break;
case 'url':
$item = sanitize_text_field($item);
if (!filter_var($item, FILTER_VALIDATE_URL) || !bpi_is_domain_allowed($item)) {
throw new Exception(__('Invalid or untrusted URL', 'bulk-plugin-installer'));
}
$result = $upgrader->install($item);
break;
case 'upload':
$file = $item;
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception(__('File upload error: ', 'bulk-plugin-installer') . $file['error']);
}
$file_name = sanitize_file_name($file['name']);
if (pathinfo($file_name, PATHINFO_EXTENSION) !== 'zip') {
throw new Exception(__('Only ZIP files are allowed', 'bulk-plugin-installer'));
}
$temp_file = $file['tmp_name'];
$upload_dir = wp_upload_dir();
$dest_path = $upload_dir['path'] . '/' . $file_name;
if (!move_uploaded_file($temp_file, $dest_path)) {
throw new Exception(__('Failed to move uploaded file. Check server permissions.', 'bulk-plugin-installer'));
}
// 检查 ZIP 文件类型
if (!$this->is_theme_zip($dest_path)) {
if ($this->is_plugin_zip($dest_path)) {
unlink($dest_path);
throw new Exception(__('This appears to be a plugin ZIP. Please use the Plugins tab to install.', 'bulk-plugin-installer'));
}
unlink($dest_path);
throw new Exception(__('Invalid theme ZIP file', 'bulk-plugin-installer'));
}
$result = $upgrader->install($dest_path);
if (file_exists($dest_path)) {
unlink($dest_path);
}
$item = $file_name;
break;
}
if (is_wp_error($result)) {
throw new Exception($result->get_error_message());
}
$results[$item] = [
'success' => $result === true,
'message' => $result === true ? __('Successfully installed', 'bulk-plugin-installer') : __('Installation failed', 'bulk-plugin-installer')
];
} catch (Exception $e) {
$results[$item] = [
'success' => false,
'message' => $e->getMessage()
];
}
}
return $results;
}
}

195
css/admin.css Normal file
View file

@ -0,0 +1,195 @@
.bpi-wrap {
margin: 20px 0;
}
.bpi-container {
max-width: unset;
}
.bpi-card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
padding: 20px;
margin-top: 20px;
}
.bpi-tabs-nav {
display: flex;
flex-wrap: wrap;
gap: 5px;
border-bottom: 1px solid #c3c4c7;
margin-bottom: 20px;
}
.bpi-tab {
padding: 8px 16px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
border-bottom: 2px solid transparent;
color: #23282d;
}
.bpi-tab.active {
border-bottom: 2px solid #007cba;
font-weight: 600;
background: #f0f0f1;
}
.bpi-tab:hover:not(.active) {
background: #f0f0f1;
border-bottom-color: #dcdcde;
}
.bpi-tab-content {
display: none;
}
.bpi-tab-content.active {
display: block;
}
.bpi-form-row {
margin: 15px 0;
}
.bpi-form-row label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.bpi-select {
min-width: 200px;
height: 35px;
padding: 0 8px;
}
.bpi-form textarea {
width: 100%;
padding: 10px;
border: 1px solid #c3c4c7;
border-radius: 4px;
}
.file-upload-container {
border: 2px dashed #b4b9be;
padding: 20px;
text-align: center;
background: #f9f9f9;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.file-upload-container:hover,
.file-upload-container.dragover {
border-color: #007cba;
background: #f0f0f1;
}
.file-upload-container input[type="file"] {
display: none;
}
.upload-instructions {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.selected-files {
margin-top: 15px;
text-align: left;
}
.selected-file {
background: #fff;
padding: 8px 12px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.selected-file .remove-file {
color: #dc3232;
cursor: pointer;
}
.source-input {
display: none;
}
.source-input.active {
display: block;
}
.bpi-results .notice,
#settings-status {
padding: 8px 12px;
border-radius: 3px;
margin-top: 10px;
}
.bpi-results .notice-success,
#settings-status.notice-success {
background-color: #dff0d8;
border-left: 4px solid #46b450;
}
.bpi-results .notice-error,
#settings-status.notice-error {
background-color: #f2dede;
border-left: 4px solid #dc3232;
}
.bpi-results .notice-info,
#settings-status.notice-info {
background-color: #e5f5fa;
border-left: 4px solid #00a0d2;
}
.installation-list {
list-style: none;
margin: 10px 0 0;
padding: 0;
}
.installation-list li {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
}
.installation-list .spinner {
margin: 0 10px 0 0;
}
.installation-list .item-name {
flex: 1;
}
.installation-list .status {
margin-left: 10px;
}
.installation-list .success {
color: #46b450;
}
.installation-list .error {
color: #dc3232;
}
.progress-count {
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
margin: 10px 0;
}

261
js/admin.js Normal file
View file

@ -0,0 +1,261 @@
jQuery(document).ready(function($) {
$('.bpi-tab').on('click', function() {
$('.bpi-tab').removeClass('active');
$(this).addClass('active');
var tab = $(this).data('tab');
$('.bpi-tab-content').removeClass('active').hide();
$('#' + tab).addClass('active').show();
});
$('.bpi-select').on('change', function() {
const $form = $(this).closest('.bpi-form');
const selectedType = $(this).val();
$form.find('.source-input').removeClass('active').hide();
$form.find('textarea[name="items"]').val('');
$form.find('input[type="file"]').val('');
$form.find('.selected-files').empty();
$form.find('.' + selectedType + '-source').addClass('active').show();
});
$('.file-upload-container').each(function() {
const $container = $(this);
const $fileInput = $container.find('input[type="file"]');
const $selectedFiles = $container.find('.selected-files');
$container.on('click', function(e) {
if (e.target === this || $(e.target).hasClass('upload-instructions')) {
$fileInput.trigger('click');
}
});
$container.on('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
$(this).addClass('dragover');
});
$container.on('dragleave drop', function(e) {
e.preventDefault();
e.stopPropagation();
$(this).removeClass('dragover');
});
$container.on('drop', function(e) {
const files = e.originalEvent.dataTransfer.files;
$fileInput[0].files = files;
handleFiles(files);
});
$fileInput.on('change', function() {
handleFiles(this.files);
});
function handleFiles(files) {
$selectedFiles.empty();
Array.from(files).forEach(file => {
if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
const $fileElement = $(`
<div class="selected-file">
<span class="filename">${escapeHtml(file.name)}</span>
<span class="remove-file dashicons dashicons-no-alt"></span>
</div>
`);
$selectedFiles.append($fileElement);
}
});
}
$selectedFiles.on('click', '.remove-file', function() {
$(this).closest('.selected-file').remove();
if ($selectedFiles.children().length === 0) {
$fileInput.val('');
}
});
});
$('#bulk-plugin-form, #bulk-theme-form').on('submit', function(e) {
e.preventDefault();
const $form = $(this);
const action = $form.attr('id') === 'bulk-plugin-form' ? 'bpi_install_plugins' : 'bpi_install_themes';
const type = $form.find('.bpi-select').val();
const $submitButton = $form.find('button[type="submit"]');
const $results = $('#installation-results');
let items = [];
let errorMessage = '';
if (type === 'upload') {
const $fileInput = $form.find('input[type="file"]');
const files = $fileInput[0].files;
if (!files || files.length === 0) {
errorMessage = 'Please select at least one ZIP file.';
} else {
items = Array.from(files).map(file => file.name);
}
} else {
const $textarea = $form.find('.' + type + '-source textarea[name="items"]');
items = $textarea.val().split('\n')
.map(item => item.trim())
.filter(item => item.length > 0);
if (items.length === 0) {
errorMessage = (type === 'repository' || type === 'wenpai') ?
'Please enter at least one slug.' :
'Please enter at least one URL.';
}
}
if (errorMessage) {
alert(errorMessage);
return;
}
$submitButton.prop('disabled', true).text('Installing...');
$results.html(`<div class="notice notice-info"><p>Installation in progress... (Large ZIP files may take some time)</p><div class="progress-count">0/${items.length} completed (0% done, ${items.length} remaining)</div><ul class="installation-list"></ul></div>`);
const $list = $results.find('.installation-list');
const $progress = $results.find('.progress-count');
let completed = 0;
if (type === 'upload') {
const formData = new FormData($form[0]);
formData.append('action', action);
formData.append('nonce', bpiAjax.nonce);
formData.append('install_type', type);
$.ajax({
url: bpiAjax.ajaxurl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
Object.keys(response.data).forEach((item, index) => {
handleResponse(response, item, index);
});
} else {
$list.append(`<li><span class="item-name">Upload Error</span><span class="status error">✗ ${response.data}</span></li>`);
}
installationComplete();
},
error: function(xhr, status, error) {
$list.append(`<li><span class="item-name">Upload Error</span><span class="status error">✗ ${xhr.responseText || error}</span></li>`);
installationComplete();
}
});
} else {
processNextItem(0);
}
function processNextItem(index) {
if (index >= items.length) {
installationComplete();
return;
}
const item = items[index];
$list.append(`<li id="item-${index}"><span class="spinner is-active"></span><span class="item-name">${escapeHtml(item)}</span><span class="status"></span></li>`);
$.ajax({
url: bpiAjax.ajaxurl,
type: 'POST',
data: {
action: action,
nonce: bpiAjax.nonce,
items: JSON.stringify([item]),
install_type: type
},
success: function(response) {
handleResponse(response, item, index);
processNextItem(index + 1);
},
error: function(xhr, status, error) {
handleError(xhr, status, error, item, index);
processNextItem(index + 1);
}
});
}
function handleResponse(response, item, index) {
const $item = $(`#item-${index}`) || $list.find('li:last');
$item.find('.spinner').removeClass('is-active');
if (response.success) {
const result = response.data[item];
$item.addClass(result.success ? 'success' : 'error')
.find('.status').text(result.success ? '✓ ' + result.message : '✗ ' + result.message);
} else {
$item.addClass('error').find('.status').text('✗ ' + (response.data || 'Unknown error'));
}
completed++;
const percentage = Math.round((completed / items.length) * 100);
const remaining = items.length - completed;
$progress.text(`${completed}/${items.length} completed (${percentage}% done, ${remaining} remaining)`);
}
function handleError(xhr, status, error, item, index) {
const $item = $(`#item-${index}`) || $list.find('li:last');
$item.find('.spinner').removeClass('is-active')
.addClass('error')
.find('.status').text(`✗ Installation failed: ${xhr.responseText || error}`);
completed++;
const percentage = Math.round((completed / items.length) * 100);
const remaining = items.length - completed;
$progress.text(`${completed}/${items.length} completed (${percentage}% done, ${remaining} remaining)`);
}
function installationComplete() {
$submitButton.prop('disabled', false).text(`Install ${action === 'bpi_install_plugins' ? 'Plugins' : 'Themes'}`);
const $notice = $results.find('.notice').removeClass('notice-info').addClass('notice-success');
$notice.find('p').text('Installation completed!');
}
});
$('#bpi-settings-form').on('submit', function(e) {
e.preventDefault();
const $form = $(this);
const $submitButton = $form.find('button[type="submit"]');
const $status = $('#settings-status');
$submitButton.prop('disabled', true).text('Saving...');
$status.removeClass('notice-success notice-error').addClass('notice-info').text('Saving...').show();
const formData = $form.serialize(); // 使用 serialize() 而不是 serializeArray()
$.ajax({
url: bpiAjax.ajaxurl,
type: 'POST',
data: {
action: 'bpi_save_settings',
nonce: bpiAjax.nonce,
bpi_allowed_roles: $form.find('input[name="bpi_allowed_roles[]"]:checked').map(function() { return this.value; }).get(),
bpi_custom_domains: $form.find('textarea[name="bpi_custom_domains"]').val()
},
success: function(response) {
if (response.success) {
$status.removeClass('notice-info').addClass('notice-success').text(response.data || 'Settings saved successfully!').show().delay(3000).fadeOut();
} else {
$status.removeClass('notice-info').addClass('notice-error').text(response.data || 'Failed to save settings.').show();
}
$submitButton.prop('disabled', false).text('Save Settings');
},
error: function(xhr, status, error) {
$status.removeClass('notice-info').addClass('notice-error').text('An error occurred while saving settings: ' + (xhr.responseText || error)).show();
$submitButton.prop('disabled', false).text('Save Settings');
}
});
});
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
});