diff --git a/css/admin.css b/assets/css/admin.css similarity index 93% rename from css/admin.css rename to assets/css/admin.css index df9124b..addb69e 100644 --- a/css/admin.css +++ b/assets/css/admin.css @@ -192,4 +192,19 @@ background: #f8f9fa; border-radius: 4px; margin: 10px 0; +} + +.retry-btn { + background: #007cba; + color: #fff; + border: none; + padding: 2px 8px; + border-radius: 3px; + cursor: pointer; + font-size: 12px; + margin-left: 10px; +} + +.retry-btn:hover { + background: #005a87; } \ No newline at end of file diff --git a/js/admin.js b/assets/js/admin.js similarity index 69% rename from js/admin.js rename to assets/js/admin.js index c2ea37c..7d91f30 100644 --- a/js/admin.js +++ b/assets/js/admin.js @@ -1,4 +1,6 @@ jQuery(document).ready(function($) { + const $results = $('#installation-results'); + $('.bpi-tab').on('click', function() { $('.bpi-tab').removeClass('active'); $(this).addClass('active'); @@ -82,7 +84,6 @@ jQuery(document).ready(function($) { 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 = ''; @@ -132,17 +133,19 @@ jQuery(document).ready(function($) { processData: false, contentType: false, success: function(response) { + console.log('Upload response:', response); // 调试输出 if (response.success) { Object.keys(response.data).forEach((item, index) => { handleResponse(response, item, index); }); } else { - $list.append(`
  • Upload Error✗ ${response.data}
  • `); + $list.append(`
  • Upload Error✗ ${escapeHtml(response.data || 'Unknown upload error')}
  • `); } installationComplete(); }, error: function(xhr, status, error) { - $list.append(`
  • Upload Error✗ ${xhr.responseText || error}
  • `); + console.log('Upload error:', xhr, status, error); // 调试输出 + $list.append(`
  • Upload Error✗ ${escapeHtml(xhr.responseText || error)}
  • `); installationComplete(); } }); @@ -169,10 +172,12 @@ jQuery(document).ready(function($) { install_type: type }, success: function(response) { + console.log('Item response:', response); // 调试输出 handleResponse(response, item, index); processNextItem(index + 1); }, error: function(xhr, status, error) { + console.log('Item error:', xhr, status, error); // 调试输出 handleError(xhr, status, error, item, index); processNextItem(index + 1); } @@ -183,12 +188,23 @@ jQuery(document).ready(function($) { const $item = $(`#item-${index}`) || $list.find('li:last'); $item.find('.spinner').removeClass('is-active'); - if (response.success) { + if (response.success && response.data[item]) { const result = response.data[item]; - $item.addClass(result.success ? 'success' : 'error') - .find('.status').text(result.success ? '✓ ' + result.message : '✗ ' + result.message); + $item.addClass(result.success ? 'success' : 'error'); + let statusHtml = ''; + if (result.success) { + statusHtml = result.skipped ? 'ⓘ ' + escapeHtml(result.message) : '✓ ' + escapeHtml(result.message); + } else { + statusHtml = '✗ ' + escapeHtml(result.message); + if (result.retry) { + statusHtml += ' '; + } + } + $item.find('.status').html(statusHtml); } else { - $item.addClass('error').find('.status').text('✗ ' + (response.data || 'Unknown error')); + $item.addClass('error') + .find('.status') + .html('✗ ' + escapeHtml(response.data || 'Unknown error') + ' '); } completed++; @@ -201,7 +217,8 @@ jQuery(document).ready(function($) { const $item = $(`#item-${index}`) || $list.find('li:last'); $item.find('.spinner').removeClass('is-active') .addClass('error') - .find('.status').text(`✗ Installation failed: ${xhr.responseText || error}`); + .find('.status') + .html(`✗ ${escapeHtml(xhr.responseText || 'Installation failed: ' + error)} `); completed++; const percentage = Math.round((completed / items.length) * 100); const remaining = items.length - completed; @@ -211,10 +228,60 @@ jQuery(document).ready(function($) { 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!'); + $notice.find('p').html('Installation completed! Check the results below. Failed items can be retried using the "Retry" buttons if applicable.'); } }); + $results.on('click', '.retry-btn', function() { + const $button = $(this); + const item = $button.data('item'); + const type = $button.data('type'); + const action = $('#bulk-plugin-form').is(':visible') ? 'bpi_install_plugins' : 'bpi_install_themes'; + const $li = $button.closest('li'); + $li.find('.spinner').addClass('is-active'); + $li.find('.status').html(''); + + $.ajax({ + url: bpiAjax.ajaxurl, + type: 'POST', + data: { + action: action, + nonce: bpiAjax.nonce, + items: JSON.stringify([item]), + install_type: type + }, + success: function(response) { + console.log('Retry response:', response); // 调试输出 + $li.find('.spinner').removeClass('is-active'); + if (response.success && response.data[item]) { + const result = response.data[item]; + $li.removeClass('error success').addClass(result.success ? 'success' : 'error'); + let statusHtml = ''; + if (result.success) { + statusHtml = result.skipped ? 'ⓘ ' + escapeHtml(result.message) : '✓ ' + escapeHtml(result.message); + } else { + statusHtml = '✗ ' + escapeHtml(result.message); + if (result.retry) { + statusHtml += ' '; + } + } + $li.find('.status').html(statusHtml); + } else { + $li.addClass('error') + .find('.status') + .html('✗ ' + escapeHtml(response.data || 'Unknown error') + ' '); + } + }, + error: function(xhr, status, error) { + console.log('Retry error:', xhr, status, error); // 调试输出 + $li.find('.spinner').removeClass('is-active') + .addClass('error') + .find('.status') + .html(`✗ ${escapeHtml(xhr.responseText || 'Retry failed: ' + error)} `); + } + }); + }); + $('#bpi-settings-form').on('submit', function(e) { e.preventDefault(); const $form = $(this); @@ -224,8 +291,6 @@ jQuery(document).ready(function($) { $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', diff --git a/bulk-plugin-installer.php b/bulk-plugin-installer.php index 39c122b..2453959 100644 --- a/bulk-plugin-installer.php +++ b/bulk-plugin-installer.php @@ -22,8 +22,8 @@ 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'; +require_once BPI_PATH . 'includes/class-installer.php'; +require_once BPI_PATH . 'includes/admin-page.php'; function bpi_init() { if (is_multisite()) { @@ -53,10 +53,10 @@ function bpi_add_menu_page() { function bpi_add_network_submenu_page() { add_submenu_page( - 'plugins.php', + 'plugins.php', __('Plugin Installer', 'bulk-plugin-installer'), __('Plugin Installer', 'bulk-plugin-installer'), - 'manage_network_plugins', + 'manage_network_plugins', 'bulk-plugin-installer', 'bpi_render_admin_page' ); @@ -69,7 +69,7 @@ define('BPI_TRUSTED_DOMAINS', [ 'github.com', 'raw.githubusercontent.com', 'wenpai.cn', - 'wenpai.net', + 'wenpai.net', 'wenpai.org', 'downloads.wenpai.net', 'weixiaoduo.com', @@ -141,7 +141,7 @@ function bpi_is_domain_allowed($url) { 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')); } @@ -201,7 +201,7 @@ function bpi_handle_install_plugins() { 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')); } @@ -322,4 +322,4 @@ function bpi_activate() { 'last_install_time' => '' ]); } -} \ No newline at end of file +} diff --git a/admin-page.php b/includes/admin-page.php similarity index 97% rename from admin-page.php rename to includes/admin-page.php index e44162b..9fe6a1c 100644 --- a/admin-page.php +++ b/includes/admin-page.php @@ -3,9 +3,9 @@ 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_enqueue_style('bpi-admin-style', BPI_URL . 'assets/css/admin.css', [], BPI_VERSION); + wp_enqueue_script('bpi-admin', BPI_URL . 'assets/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') @@ -43,21 +43,21 @@ function bpi_render_admin_page() {
    -
    -
    -
    @@ -98,21 +98,21 @@ function bpi_render_admin_page() {
    -
    -
    -
    @@ -222,4 +222,4 @@ function bpi_render_admin_page() { wp_filesystem = $wp_filesystem; @@ -48,11 +48,11 @@ class BPI_Installer { 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')]; + return ['error' => 'Invalid installation type']; } if (empty($items) || !is_array($items)) { - return ['error' => __('No items provided', 'bulk-plugin-installer')]; + return ['error' => 'No items provided']; } require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; @@ -69,7 +69,7 @@ class BPI_Installer { 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')); + throw new Exception('Invalid plugin slug', 1001); } $plugin_key = $item . '/' . $item . '.php'; } elseif ($type === 'upload') { @@ -80,11 +80,13 @@ class BPI_Installer { if ($plugin_key && array_key_exists($plugin_key, $installed_plugins)) { $results[$item] = [ 'success' => true, - 'message' => __('Plugin already installed, skipped', 'bulk-plugin-installer') + 'message' => 'Plugin already installed, skipped', + 'skipped' => true ]; continue; } + $download_link = ''; switch ($type) { case 'repository': $api = plugins_api('plugin_information', [ @@ -92,78 +94,88 @@ class BPI_Installer { 'fields' => ['sections' => false] ]); if (is_wp_error($api)) { - throw new Exception($api->get_error_message()); + throw new Exception('Failed to fetch plugin info: ' . $api->get_error_message(), 1002); } - $result = $upgrader->install($api->download_link); + $download_link = $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()); + $code = wp_remote_retrieve_response_code($response); + throw new Exception("Download failed with status code $code: " . $response->get_error_message(), 1003); } $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')); + if (!filter_var($item, FILTER_VALIDATE_URL)) { + throw new Exception('Invalid URL format', 1004); } - $result = $upgrader->install($item); + if (!bpi_is_domain_allowed($item)) { + throw new Exception('Untrusted domain', 1005); + } + $download_link = $item; break; case 'upload': $file = $item; if ($file['error'] !== UPLOAD_ERR_OK) { - throw new Exception(__('File upload error: ', 'bulk-plugin-installer') . $file['error']); + throw new Exception('File upload error: ' . $file['error'], 1006); } $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')); + throw new Exception('Only ZIP files are allowed', 1007); } $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')); + throw new Exception('Failed to move uploaded file. Check server permissions.', 1008); } - // 检查 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')); + throw new Exception('This appears to be a theme ZIP. Please use the Plugins tab to install.', 1009); } unlink($dest_path); - throw new Exception(__('Invalid plugin ZIP file', 'bulk-plugin-installer')); + throw new Exception('Invalid plugin ZIP file', 1010); } - $result = $upgrader->install($dest_path); - if (file_exists($dest_path)) { - unlink($dest_path); - } + $download_link = $dest_path; $item = $file_name; break; } + $result = $upgrader->install($download_link); + if ($type === 'upload' && file_exists($dest_path)) { + unlink($dest_path); + } + if (is_wp_error($result)) { - throw new Exception($result->get_error_message()); + $error_message = $result->get_error_message(); + throw new Exception($error_message ?: 'Unknown installation error', 1011); + } elseif ($result !== true) { + throw new Exception('Installation failed unexpectedly', 1012); } $results[$item] = [ - 'success' => $result === true, - 'message' => $result === true ? __('Successfully installed', 'bulk-plugin-installer') : __('Installation failed', 'bulk-plugin-installer') + 'success' => true, + 'message' => 'Successfully installed' ]; } catch (Exception $e) { + $no_retry_codes = [1001, 1004, 1005, 1007, 1009, 1010]; // 无需重试的错误代码 $results[$item] = [ 'success' => false, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), + 'error_code' => $e->getCode(), + 'retry' => !in_array($e->getCode(), $no_retry_codes) ]; } } @@ -174,11 +186,11 @@ class BPI_Installer { 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')]; + return ['error' => 'Invalid installation type']; } if (empty($items) || !is_array($items)) { - return ['error' => __('No items provided', 'bulk-plugin-installer')]; + return ['error' => 'No items provided']; } require_once ABSPATH . 'wp-admin/includes/theme-install.php'; @@ -191,23 +203,79 @@ class BPI_Installer { foreach ($items as $item) { try { $theme_key = null; + $download_link = ''; 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')); + throw new Exception('Invalid theme slug', 2001); } $theme_key = $item; } elseif ($type === 'upload') { - $file_name = sanitize_file_name($item['name']); - $theme_key = pathinfo($file_name, PATHINFO_FILENAME); - } + $file = $item; + if ($file['error'] !== UPLOAD_ERR_OK) { + throw new Exception('File upload error: ' . $file['error'], 2002); + } + $file_name = sanitize_file_name($file['name']); + if (pathinfo($file_name, PATHINFO_EXTENSION) !== 'zip') { + throw new Exception('Only ZIP files are allowed', 2003); + } + $temp_file = $file['tmp_name']; + $upload_dir = wp_upload_dir(); + $dest_path = $upload_dir['path'] . '/' . $file_name; - if ($theme_key && array_key_exists($theme_key, $installed_themes)) { - $results[$item] = [ - 'success' => true, - 'message' => __('Theme already installed, skipped', 'bulk-plugin-installer') - ]; - continue; + if (!move_uploaded_file($temp_file, $dest_path)) { + throw new Exception('Failed to move uploaded file. Check server permissions.', 2004); + } + + $zip = new ZipArchive(); + if ($zip->open($dest_path) === true) { + $theme_key = null; + for ($i = 0; $i < $zip->numFiles; $i++) { + $filename = $zip->getNameIndex($i); + if (strpos($filename, 'style.css') !== false) { + $theme_key = dirname($filename); + if ($theme_key === '.') { + $theme_key = pathinfo($file_name, PATHINFO_FILENAME); + } + break; + } + } + $zip->close(); + } + + if ($theme_key && array_key_exists($theme_key, $installed_themes)) { + if (file_exists($dest_path)) { + unlink($dest_path); + } + $results[$file_name] = [ + 'success' => true, + 'message' => 'Theme already installed, skipped', + 'skipped' => true + ]; + continue; + } + + 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.', 2005); + } + unlink($dest_path); + throw new Exception('Invalid theme ZIP file', 2006); + } + + $download_link = $dest_path; + $item = $file_name; + } else { + $theme_key = $item; + if ($theme_key && array_key_exists($theme_key, $installed_themes)) { + $results[$item] = [ + 'success' => true, + 'message' => 'Theme already installed, skipped', + 'skipped' => true + ]; + continue; + } } switch ($type) { @@ -217,78 +285,62 @@ class BPI_Installer { 'fields' => ['sections' => false] ]); if (is_wp_error($api)) { - throw new Exception($api->get_error_message()); + throw new Exception('Failed to fetch theme info: ' . $api->get_error_message(), 2007); } - $result = $upgrader->install($api->download_link); + $download_link = $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()); + $code = wp_remote_retrieve_response_code($response); + throw new Exception("Download failed with status code $code: " . $response->get_error_message(), 2008); } $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')); + if (!filter_var($item, FILTER_VALIDATE_URL)) { + throw new Exception('Invalid URL format', 2009); } - $result = $upgrader->install($item); + if (!bpi_is_domain_allowed($item)) { + throw new Exception('Untrusted domain', 2010); + } + $download_link = $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; } + $result = $upgrader->install($download_link); + if ($type === 'upload' && file_exists($dest_path)) { + unlink($dest_path); + } + if (is_wp_error($result)) { - throw new Exception($result->get_error_message()); + $error_message = $result->get_error_message(); + throw new Exception($error_message ?: 'Unknown installation error', 2011); + } elseif ($result !== true) { + throw new Exception('Installation failed unexpectedly', 2012); } $results[$item] = [ - 'success' => $result === true, - 'message' => $result === true ? __('Successfully installed', 'bulk-plugin-installer') : __('Installation failed', 'bulk-plugin-installer') + 'success' => true, + 'message' => 'Successfully installed' ]; } catch (Exception $e) { + $no_retry_codes = [2001, 2003, 2005, 2006, 2009, 2010]; // 无需重试的错误代码 $results[$item] = [ 'success' => false, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), + 'error_code' => $e->getCode(), + 'retry' => !in_array($e->getCode(), $no_retry_codes) ]; } } diff --git a/installer-server.json b/installer-server.json new file mode 100644 index 0000000..5b9a932 --- /dev/null +++ b/installer-server.json @@ -0,0 +1,31 @@ +{ + "plugins": { + "repository": [ + "woocommerce", + "wordpress-seo", + "elementor" + ], + "wenpai": [ + "wpfanyi-import", + "wpavatar" + ], + "url": [ + "https://downloads.wenpai.net/plugin/custom-plugin.zip", + "https://github.com/user/plugin/releases/download/v1.0/plugin.zip" + ] + }, + "themes": { + "repository": [ + "twentytwentyfive", + "astra" + ], + "wenpai": [ + "weicommerce", + "justnote" + ], + "url": [ + "https://downloads.wenpai.net/theme/custom-theme.zip", + "https://github.com/user/theme/releases/download/v1.0/theme.zip" + ] + } +}