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"
+ ]
+ }
+}