diff --git a/admin-page.php b/admin-page.php new file mode 100644 index 0000000..e44162b --- /dev/null +++ b/admin-page.php @@ -0,0 +1,225 @@ + wp_create_nonce('bpi_installer'), + 'ajaxurl' => admin_url('admin-ajax.php') + ]); + ?> +
+

+ + + +

+ +
+
+
+
+ + + +
+ +
+

+

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+
+ +
+ + +
+
+
+ +
+

+

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+
+ +
+ + +
+
+
+ +
+

+

+ + +

+ +
+ +
+ + get_names(); + foreach ($roles as $role => $label) { + printf( + '
', + esc_attr($role), + in_array($role, $allowed_roles) ? 'checked' : '', + esc_html($label) + ); + } + ?> +
+
+ + +

+ +

+
+
+ +
+
+ +
+
+ +
+
+ +
+

+

+ 0, + 'successful_installs' => 0, + 'failed_installs' => 0, + 'last_install_time' => '' + ]); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ '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' => '' + ]); + } +} \ No newline at end of file diff --git a/class-installer.php b/class-installer.php new file mode 100644 index 0000000..f7236fe --- /dev/null +++ b/class-installer.php @@ -0,0 +1,298 @@ +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; + } +} \ No newline at end of file diff --git a/css/admin.css b/css/admin.css new file mode 100644 index 0000000..df9124b --- /dev/null +++ b/css/admin.css @@ -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; +} \ No newline at end of file diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..c2ea37c --- /dev/null +++ b/js/admin.js @@ -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 = $(` +
+ ${escapeHtml(file.name)} + +
+ `); + $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(`

Installation in progress... (Large ZIP files may take some time)

0/${items.length} completed (0% done, ${items.length} remaining)
`); + + 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(`
  • Upload Error✗ ${response.data}
  • `); + } + installationComplete(); + }, + error: function(xhr, status, error) { + $list.append(`
  • Upload Error✗ ${xhr.responseText || error}
  • `); + installationComplete(); + } + }); + } else { + processNextItem(0); + } + + function processNextItem(index) { + if (index >= items.length) { + installationComplete(); + return; + } + + const item = items[index]; + $list.append(`
  • ${escapeHtml(item)}
  • `); + + $.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, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } +}); \ No newline at end of file