diff --git a/assets/css/admin.css b/assets/css/admin.css
index 9b31653..dd3e6da 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -1,3 +1,8 @@
+/**
+ * Admin styles for Bulk Plugin Installer
+ */
+
+/* General Layout */
.bpi-wrap {
margin: 20px 0;
}
@@ -12,8 +17,10 @@
border-radius: 4px;
padding: 20px;
margin-top: 20px;
+ box-shadow: 0 1px 1px rgba(0,0,0,0.04);
}
+/* Tabs Navigation */
.bpi-tabs-nav {
display: flex;
flex-wrap: wrap;
@@ -30,10 +37,11 @@
font-size: 14px;
border-bottom: 2px solid transparent;
color: #23282d;
+ transition: all 0.2s ease;
}
.bpi-tab.active {
- border-bottom: 2px solid #007cba;
+ border-bottom: 2px solid #2271b1;
font-weight: 600;
background: #f0f0f1;
}
@@ -45,12 +53,19 @@
.bpi-tab-content {
display: none;
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
}
.bpi-tab-content.active {
display: block;
}
+/* Form Elements */
.bpi-form-row {
margin: 15px 0;
}
@@ -74,6 +89,7 @@
border-radius: 4px;
}
+/* File Upload */
.file-upload-container {
border: 2px dashed #b4b9be;
padding: 20px;
@@ -86,7 +102,7 @@
.file-upload-container:hover,
.file-upload-container.dragover {
- border-color: #007cba;
+ border-color: #2271b1;
background: #f0f0f1;
}
@@ -121,6 +137,7 @@
cursor: pointer;
}
+/* Source Inputs */
.source-input {
display: none;
}
@@ -129,6 +146,7 @@
display: block;
}
+/* Notifications */
.bpi-results .notice,
#settings-status {
padding: 8px 12px;
@@ -138,13 +156,13 @@
.bpi-results .notice-success,
#settings-status.notice-success {
- background-color: #dff0d8;
+ background-color: #edfaef;
border-left: 4px solid #46b450;
}
.bpi-results .notice-error,
#settings-status.notice-error {
- background-color: #f2dede;
+ background-color: #fcf0f1;
border-left: 4px solid #dc3232;
}
@@ -154,6 +172,7 @@
border-left: 4px solid #00a0d2;
}
+/* Installation List */
.installation-list {
list-style: none;
margin: 10px 0 0;
@@ -165,6 +184,14 @@
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
+ border-radius: 4px;
+ background: #f8f9fa;
+ margin-bottom: 5px;
+ transition: background 0.2s;
+}
+
+.installation-list li:hover {
+ background: #f0f0f1;
}
.installation-list .spinner {
@@ -187,6 +214,7 @@
color: #dc3232;
}
+/* Progress Indicator */
.progress-count {
padding: 10px;
background: #f8f9fa;
@@ -194,8 +222,29 @@
margin: 10px 0;
}
+.progress-bar-container {
+ height: 20px;
+ background-color: #eee;
+ border-radius: 10px;
+ margin: 10px 0;
+ overflow: hidden;
+}
+
+.progress-bar {
+ height: 100%;
+ background-color: #2271b1;
+ border-radius: 10px;
+ text-align: center;
+ line-height: 20px;
+ color: #fff;
+ font-size: 12px;
+ font-weight: bold;
+ transition: width 0.5s ease;
+}
+
+/* Action Buttons */
.retry-btn {
- background: #007cba;
+ background: #2271b1;
color: #fff;
border: none;
padding: 2px 8px;
@@ -206,10 +255,520 @@
}
.retry-btn:hover {
- background: #005a87;
+ background: #135e96;
}
.upload.button {
margin-left: 10px;
vertical-align: top;
-}
\ No newline at end of file
+}
+
+/* Collections Styles */
+.bpi-collections-wrapper {
+ position: relative;
+ min-height: 200px;
+}
+
+.bpi-collections-wrapper.loading:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.7);
+ z-index: 1;
+}
+
+.bpi-collections-wrapper.loading:after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 30px;
+ height: 30px;
+ margin: -15px 0 0 -15px;
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #2271b1;
+ border-radius: 50%;
+ z-index: 2;
+ animation: bpi-spin 1s linear infinite;
+}
+
+@keyframes bpi-spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.bpi-collections-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.bpi-collections-filters {
+ display: flex;
+ gap: 10px;
+}
+
+.bpi-collections-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.bpi-collection-card {
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ padding: 20px;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.bpi-collection-card:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+}
+
+.bpi-collection-icon {
+ margin-bottom: 15px;
+}
+
+.bpi-collection-icon .dashicons {
+ font-size: 36px;
+ width: 36px;
+ height: 36px;
+ color: #2271b1;
+}
+
+.bpi-collection-card h3 {
+ margin: 0 0 10px 0;
+ font-size: 18px;
+ color: #23282d;
+}
+
+.bpi-collection-card p {
+ color: #646970;
+ margin: 0 0 15px 0;
+ font-size: 13px;
+}
+
+.bpi-collection-meta {
+ display: flex;
+ gap: 15px;
+ margin-bottom: 15px;
+ font-size: 12px;
+ color: #50575e;
+}
+
+.bpi-back-to-collections {
+ background: none;
+ border: none;
+ padding: 10px 0;
+ cursor: pointer;
+ color: #2271b1;
+ display: inline-flex;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.bpi-back-to-collections:hover {
+ color: #135e96;
+}
+
+.bpi-back-to-collections .dashicons {
+ margin-right: 5px;
+}
+
+.bpi-collection-details h2 {
+ margin-top: 0;
+}
+
+.bpi-collection-plugins, .bpi-collection-themes {
+ margin-top: 20px;
+}
+
+.bpi-collection-plugins h3, .bpi-collection-themes h3 {
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #eee;
+}
+
+.bpi-collection-item-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 15px;
+}
+
+.bpi-collection-item {
+ background: #f8f9fa;
+ padding: 10px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+}
+
+.bpi-collection-item-icon {
+ width: 24px;
+ height: 24px;
+ margin-right: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #e9f0f5;
+ border-radius: 4px;
+ color: #2271b1;
+}
+
+.bpi-collection-item-name {
+ font-size: 13px;
+ flex-grow: 1;
+}
+
+.bpi-collection-item-description {
+ font-size: 11px;
+ color: #666;
+ margin-top: 4px;
+}
+
+.bpi-required {
+ font-size: 11px;
+ color: #d63638;
+ margin-left: 5px;
+}
+
+.bpi-collection-item-source {
+ font-size: 10px;
+ color: #777;
+ background: #eee;
+ padding: 2px 5px;
+ border-radius: 3px;
+}
+
+.bpi-install-collection-wrapper {
+ background: #f0f6fc;
+ padding: 15px;
+ border-left: 4px solid #2271b1;
+ margin: 20px 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.bpi-install-collection-wrapper p {
+ margin: 0;
+ flex-grow: 1;
+}
+
+.bpi-collection-screenshot {
+ margin: 15px 0;
+ text-align: center;
+}
+
+.bpi-collection-screenshot img {
+ max-width: 100%;
+ border-radius: 4px;
+ border: 1px solid #ddd;
+}
+
+.bpi-empty-collections {
+ grid-column: 1 / -1;
+ padding: 30px;
+ text-align: center;
+ background: #f8f9fa;
+ border-radius: 4px;
+ color: #646970;
+}
+
+/* Settings - Collection Sources */
+.bpi-sources-table {
+ margin-top: 15px;
+}
+
+.bpi-toggle {
+ position: relative;
+ display: inline-block;
+ width: 40px;
+ height: 20px;
+}
+
+.bpi-toggle input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.bpi-toggle-slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ transition: .4s;
+ border-radius: 34px;
+}
+
+.bpi-toggle-slider:before {
+ position: absolute;
+ content: "";
+ height: 16px;
+ width: 16px;
+ left: 2px;
+ bottom: 2px;
+ background-color: white;
+ transition: .4s;
+ border-radius: 50%;
+}
+
+input:checked + .bpi-toggle-slider {
+ background-color: #2271b1;
+}
+
+input:checked + .bpi-toggle-slider:before {
+ transform: translateX(20px);
+}
+
+/* Logs styles */
+.bpi-log-filters {
+ margin-bottom: 20px;
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 4px;
+}
+
+.bpi-filter-row {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.bpi-status {
+ padding: 3px 8px;
+ border-radius: 3px;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.bpi-status-success {
+ background-color: #edfaef;
+ color: #46b450;
+}
+
+.bpi-status-error {
+ background-color: #fcf0f1;
+ color: #dc3232;
+}
+
+.bpi-status-skipped {
+ background-color: #f8f9fa;
+ color: #555;
+}
+
+.bpi-log-actions {
+ margin-top: 20px;
+ display: flex;
+ gap: 10px;
+}
+
+.bpi-radio-group {
+ margin-bottom: 15px;
+}
+
+.bpi-radio-group label {
+ display: inline-block;
+ margin-right: 15px;
+ font-weight: normal;
+}
+
+/* Reinstall tab styles */
+.bpi-reinstall-tabs {
+ display: flex;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #ddd;
+}
+
+.bpi-reinstall-tab {
+ padding: 10px 15px;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ margin-right: 10px;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.bpi-reinstall-tab.active {
+ border-bottom-color: #2271b1;
+ color: #2271b1;
+}
+
+.bpi-reinstall-content {
+ display: none;
+}
+
+.bpi-reinstall-content.active {
+ display: block;
+}
+
+.bpi-reinstall-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ margin-bottom: 15px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #eee;
+}
+
+.bpi-search-box {
+ flex: 1;
+}
+
+.bpi-search-box input {
+ width: 100%;
+ padding: 6px 8px;
+}
+
+.bpi-filter-actions,
+.bpi-selection-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+}
+
+.bpi-plugins-table,
+.bpi-themes-table {
+ margin-bottom: 20px;
+}
+
+.bpi-plugins-table .check-column,
+.bpi-themes-table .check-column {
+ width: 2%;
+}
+
+.plugin-description,
+.theme-description {
+ color: #646970;
+ font-size: 12px;
+ margin-top: 3px;
+}
+
+.bpi-source {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 12px;
+}
+
+.source-wp {
+ background-color: #e5f7ff;
+ color: #0073aa;
+}
+
+.source-wenpai {
+ background-color: #edf7ed;
+ color: #298029;
+}
+
+.source-unknown {
+ background-color: #f8f0e3;
+ color: #c25000;
+}
+
+.status-active {
+ color: #008a20;
+}
+
+.status-inactive {
+ color: #646970;
+}
+
+.bpi-manual-note {
+ font-size: 11px;
+ color: #646970;
+ font-style: italic;
+}
+
+.bpi-bulk-actions {
+ margin-top: 20px;
+ display: flex;
+ align-items: flex-start;
+ gap: 15px;
+}
+
+.bpi-warning {
+ background: #fff8e5;
+ border-left: 4px solid #ffb900;
+ padding: 8px 12px;
+ margin: 0;
+ font-size: 13px;
+ align-self: center;
+}
+
+.bpi-warning .dashicons {
+ color: #ffb900;
+ margin-right: 5px;
+}
+
+/* Responsive Styles */
+@media (max-width: 782px) {
+ .bpi-collections-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .bpi-collection-item-list {
+ grid-template-columns: 1fr;
+ }
+
+ .bpi-install-collection-wrapper {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .bpi-install-collection-wrapper .button {
+ margin-top: 10px;
+ width: 100%;
+ text-align: center;
+ }
+
+ .bpi-collections-controls {
+ flex-direction: column;
+ gap: 10px;
+ align-items: flex-start;
+ }
+
+ .bpi-collections-filters {
+ width: 100%;
+ }
+
+ .bpi-collections-filters select {
+ flex: 1;
+ }
+
+ .bpi-filter-row {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .bpi-filter-row > * {
+ width: 100%;
+ margin-bottom: 5px;
+ }
+
+ .bpi-log-actions {
+ flex-direction: column;
+ }
+
+ .bpi-log-actions .button {
+ width: 100%;
+ text-align: center;
+ margin-bottom: 5px;
+ }
+}
diff --git a/assets/js/admin.js b/assets/js/admin.js
index d52ae5d..126eef7 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -1,3 +1,6 @@
+/**
+ * Admin JavaScript functionality
+ */
jQuery(document).ready(function($) {
const $results = $('#installation-results');
@@ -11,27 +14,35 @@ jQuery(document).ready(function($) {
$('#' + tab).addClass('active').show();
}
+ // Tab navigation
$('.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();
+
+ // Update URL without page reload
+ const url = new URL(window.location);
+ url.searchParams.set('tab', tab);
+ window.history.pushState({}, '', url);
});
+ // Installation source selection
$('.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 handling
$('.file-upload-container').each(function() {
const $container = $(this);
const $fileInput = $container.find('input[type="file"]');
@@ -88,6 +99,7 @@ jQuery(document).ready(function($) {
});
});
+ // Plugin/Theme installation form submission
$('#bulk-plugin-form, #bulk-theme-form').on('submit', function(e) {
e.preventDefault();
const $form = $(this);
@@ -112,8 +124,8 @@ jQuery(document).ready(function($) {
.map(item => item.trim())
.filter(item => item.length > 0);
if (items.length === 0) {
- errorMessage = (type === 'repository' || type === 'wenpai') ?
- bpiAjax.i18n.no_slugs :
+ errorMessage = (type === 'repository' || type === 'wenpai') ?
+ bpiAjax.i18n.no_slugs :
bpiAjax.i18n.no_urls;
}
}
@@ -124,10 +136,18 @@ jQuery(document).ready(function($) {
}
$submitButton.prop('disabled', true).text('Installing...');
- $results.html(`
${bpiAjax.i18n.installation_in_progress}
0/${items.length} ${bpiAjax.i18n.completed} (0% done, ${items.length} ${bpiAjax.i18n.remaining})
`);
+ $results.html(`
+
${bpiAjax.i18n.installation_in_progress}
+
+
0/${items.length} ${bpiAjax.i18n.completed} (0% done, ${items.length} ${bpiAjax.i18n.remaining})
+
+
`);
const $list = $results.find('.installation-list');
const $progress = $results.find('.progress-count');
+ const $progressBar = $results.find('.progress-bar');
let completed = 0;
if (type === 'upload') {
@@ -143,7 +163,6 @@ 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);
@@ -154,7 +173,6 @@ jQuery(document).ready(function($) {
installationComplete();
},
error: function(xhr, status, error) {
- console.log('Upload error:', xhr, status, error);
$list.append(`${escapeHtml('Upload Error')}✗ ${escapeHtml(xhr.responseText || error)}`);
installationComplete();
}
@@ -182,12 +200,10 @@ 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);
}
@@ -217,39 +233,44 @@ jQuery(document).ready(function($) {
.html('✗ ' + escapeHtml(response.data || 'Unknown error') + ' ');
}
- completed++;
- const percentage = Math.round((completed / items.length) * 100);
- const remaining = items.length - completed;
- $progress.text(`${completed}/${items.length} ${bpiAjax.i18n.completed} (${percentage}% done, ${remaining} ${bpiAjax.i18n.remaining})`);
+ updateProgress();
}
function handleError(xhr, status, error, item, index) {
const $item = $(`#item-${index}`) || $list.find('li:last');
- $item.find('.spinner').removeClass('is-active')
- .addClass('error')
+ $item.find('.spinner').removeClass('is-active');
+ $item.addClass('error')
.find('.status')
.html(`✗ ${escapeHtml(xhr.responseText || 'Installation failed: ' + error)} `);
+
+ updateProgress();
+ }
+
+ function updateProgress() {
completed++;
const percentage = Math.round((completed / items.length) * 100);
const remaining = items.length - completed;
$progress.text(`${completed}/${items.length} ${bpiAjax.i18n.completed} (${percentage}% done, ${remaining} ${bpiAjax.i18n.remaining})`);
+ $progressBar.css('width', percentage + '%').text(percentage + '%');
}
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').html(bpiAjax.i18n.installation_complete);
+ $progressBar.css('width', '100%').text('100%');
}
});
+ // Retry button handler
$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('');
+
+ $li.find('.status').html('');
$.ajax({
url: bpiAjax.ajaxurl,
@@ -261,7 +282,6 @@ jQuery(document).ready(function($) {
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];
@@ -283,15 +303,15 @@ jQuery(document).ready(function($) {
}
},
error: function(xhr, status, error) {
- console.log('Retry error:', xhr, status, error);
- $li.find('.spinner').removeClass('is-active')
- .addClass('error')
+ $li.find('.spinner').removeClass('is-active');
+ $li.addClass('error')
.find('.status')
.html(`✗ ${escapeHtml(xhr.responseText || 'Retry failed: ' + error)} `);
}
});
});
+ // Settings form submission
$('#bpi-settings-form').on('submit', function(e) {
e.preventDefault();
const $form = $(this);
@@ -301,6 +321,13 @@ jQuery(document).ready(function($) {
$submitButton.prop('disabled', true).text('Saving...');
$status.removeClass('notice-success notice-error').addClass('notice-info').text('Saving...').show();
+ // Get installation options
+ const installOptions = {
+ duplicate_handling: $form.find('input[name="bpi_install_options[duplicate_handling]"]:checked').val(),
+ auto_activate: $form.find('input[name="bpi_install_options[auto_activate]"]').is(':checked') ? 1 : 0,
+ keep_backups: $form.find('input[name="bpi_install_options[keep_backups]"]').is(':checked') ? 1 : 0
+ };
+
$.ajax({
url: bpiAjax.ajaxurl,
type: 'POST',
@@ -308,7 +335,8 @@ jQuery(document).ready(function($) {
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()
+ bpi_custom_domains: $form.find('textarea[name="bpi_custom_domains"]').val(),
+ bpi_install_options: installOptions
},
success: function(response) {
if (response.success) {
@@ -325,6 +353,281 @@ jQuery(document).ready(function($) {
});
});
+ // Collection view handlers
+ $(document).on('click', '.bpi-load-collection', function() {
+ const collectionId = $(this).data('collection');
+
+ // Show loading animation
+ $('.bpi-collections-wrapper').addClass('loading');
+
+ // AJAX get collection details
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_get_collection_details',
+ nonce: bpiAjax.nonce,
+ collection_id: collectionId
+ },
+ success: function(response) {
+ if (response.success) {
+ // Fill collection details content
+ $('#bpi-collection-content').html(response.data.html);
+
+ // Show details view and hide list view
+ $('.bpi-collections-grid').hide();
+ $('.bpi-collections-controls').hide();
+ $('.bpi-collection-details').show();
+ } else {
+ alert(response.data.message || 'Failed to load collection');
+ }
+ $('.bpi-collections-wrapper').removeClass('loading');
+ },
+ error: function() {
+ alert('An error occurred while loading the collection');
+ $('.bpi-collections-wrapper').removeClass('loading');
+ }
+ });
+ });
+
+ // Return to collection list
+ $(document).on('click', '.bpi-back-to-collections', function() {
+ $('.bpi-collection-details').hide();
+ $('.bpi-collections-grid').show();
+ $('.bpi-collections-controls').show();
+ });
+
+ // Install collection
+ $(document).on('click', '.bpi-install-collection', function() {
+ const collectionId = $(this).data('collection');
+
+ if (!confirm(bpiAjax.i18n.confirm_install_collection)) {
+ return;
+ }
+
+ const $button = $(this);
+ $button.prop('disabled', true).text(bpiAjax.i18n.installing);
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_install_collection',
+ nonce: bpiAjax.nonce,
+ collection_id: collectionId
+ },
+ success: function(response) {
+ if (response.success) {
+ $('#installation-results').html(response.data.html);
+ $('html, body').animate({
+ scrollTop: $('#installation-results').offset().top
+ }, 500);
+ } else {
+ alert(response.data.message || 'Failed to install collection');
+ }
+ $button.prop('disabled', false).text(bpiAjax.i18n.install_collection);
+ },
+ error: function() {
+ alert('An error occurred during installation');
+ $button.prop('disabled', false).text(bpiAjax.i18n.install_collection);
+ }
+ });
+ });
+
+ // Collection filtering
+ $('#collection-category-filter, #collection-level-filter').on('change', function() {
+ filterCollections();
+ });
+
+ function filterCollections() {
+ const category = $('#collection-category-filter').val();
+ const level = $('#collection-level-filter').val();
+
+ $('.bpi-collection-card').each(function() {
+ const $card = $(this);
+ const cardCategory = $card.data('category');
+ const cardLevel = $card.data('level');
+
+ let show = true;
+
+ if (category && cardCategory !== category) {
+ show = false;
+ }
+
+ if (level && cardLevel !== level) {
+ show = false;
+ }
+
+ $card.toggle(show);
+ });
+
+ // Show message if no results
+ if ($('.bpi-collection-card:visible').length === 0) {
+ if ($('.bpi-empty-collections').length === 0) {
+ $('.bpi-collections-grid').append('' +
+ 'No collections match your selected filters. Please try different filter options.' +
+ '
');
+ }
+ } else {
+ $('.bpi-empty-collections').remove();
+ }
+ }
+
+ // Refresh collections
+ $('.bpi-refresh-collections').on('click', function() {
+ const $button = $(this);
+ $button.prop('disabled', true);
+ $('.bpi-collections-wrapper').addClass('loading');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_refresh_collections',
+ nonce: bpiAjax.nonce
+ },
+ success: function(response) {
+ if (response.success) {
+ window.location.reload();
+ } else {
+ alert(response.data.message || 'Failed to refresh collections');
+ $button.prop('disabled', false);
+ $('.bpi-collections-wrapper').removeClass('loading');
+ }
+ },
+ error: function() {
+ alert('An error occurred while refreshing collections');
+ $button.prop('disabled', false);
+ $('.bpi-collections-wrapper').removeClass('loading');
+ }
+ });
+ });
+
+ // Collection source management
+ $('#bpi-add-source-form').on('submit', function(e) {
+ e.preventDefault();
+ const $form = $(this);
+ const $submitBtn = $('#source-submit-btn');
+ const $nameInput = $('#source-name');
+ const $urlInput = $('#source-url');
+ const sourceId = $('#source-id').val();
+
+ $submitBtn.prop('disabled', true).text('Saving...');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_save_remote_source',
+ nonce: bpiAjax.nonce,
+ name: $nameInput.val(),
+ url: $urlInput.val(),
+ id: sourceId
+ },
+ success: function(response) {
+ if (response.success) {
+ window.location.reload();
+ } else {
+ alert(response.data.message || 'Failed to save source');
+ $submitBtn.prop('disabled', false).text(sourceId ? 'Update Source' : 'Add Source');
+ }
+ },
+ error: function() {
+ alert('An error occurred while saving the source');
+ $submitBtn.prop('disabled', false).text(sourceId ? 'Update Source' : 'Add Source');
+ }
+ });
+ });
+
+ // Edit source
+ $('.bpi-edit-source').on('click', function() {
+ const id = $(this).data('id');
+ const name = $(this).data('name');
+ const url = $(this).data('url');
+
+ $('#source-id').val(id);
+ $('#source-name').val(name);
+ $('#source-url').val(url);
+ $('#source-submit-btn').text('Update Source');
+ $('#source-cancel-btn').show();
+
+ $('html, body').animate({
+ scrollTop: $('#bpi-add-source-form').offset().top
+ }, 500);
+ });
+
+ // Cancel edit
+ $('#source-cancel-btn').on('click', function() {
+ $('#source-id').val('');
+ $('#source-name').val('');
+ $('#source-url').val('');
+ $('#source-submit-btn').text('Add Source');
+ $(this).hide();
+ });
+
+ // Delete source
+ $('.bpi-delete-source').on('click', function() {
+ if (!confirm(bpiAjax.i18n.confirm_delete_source)) {
+ return;
+ }
+
+ const id = $(this).data('id');
+ const $row = $(this).closest('tr');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_delete_remote_source',
+ nonce: bpiAjax.nonce,
+ id: id
+ },
+ success: function(response) {
+ if (response.success) {
+ $row.fadeOut(400, function() {
+ $(this).remove();
+ if ($('.bpi-sources-table tbody tr').length === 0) {
+ $('.bpi-sources-table tbody').html('' +
+ 'No sources added yet. Add a source below.' +
+ ' |
');
+ }
+ });
+ } else {
+ alert(response.data.message || 'Failed to delete source');
+ }
+ },
+ error: function() {
+ alert('An error occurred while deleting the source');
+ }
+ });
+ });
+
+ // Toggle source enabled/disabled
+ $('.bpi-source-toggle').on('change', function() {
+ const id = $(this).data('id');
+ const enabled = $(this).prop('checked');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_toggle_remote_source',
+ nonce: bpiAjax.nonce,
+ id: id,
+ enabled: enabled
+ },
+ success: function(response) {
+ if (!response.success) {
+ alert(response.data.message || 'Failed to update source status');
+ }
+ },
+ error: function() {
+ alert('An error occurred while updating the source');
+ }
+ });
+ });
+
+ // Helper function to escape HTML
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
@@ -333,4 +636,555 @@ jQuery(document).ready(function($) {
.replace(/"/g, """)
.replace(/'/g, "'");
}
-});
\ No newline at end of file
+
+ // Reinstall tab functionality
+ (function() {
+ // Only run if the reinstall tab exists
+ if ($('#reinstall').length === 0) return;
+
+ // Tab switching within reinstall tab
+ $('.bpi-reinstall-tab').on('click', function() {
+ $('.bpi-reinstall-tab').removeClass('active');
+ $(this).addClass('active');
+
+ const contentId = $(this).data('content');
+ $('.bpi-reinstall-content').removeClass('active');
+ $(`#bpi-reinstall-${contentId}`).addClass('active');
+ });
+
+ // Load plugins and themes on tab activation
+ $('.bpi-tab[data-tab="reinstall"]').on('click', function() {
+ loadPlugins();
+ loadThemes();
+ });
+
+ // Also load on page load if reinstall tab is active
+ if ($('.bpi-tab[data-tab="reinstall"]').hasClass('active')) {
+ loadPlugins();
+ loadThemes();
+ }
+
+ // Function to load installed plugins
+ function loadPlugins() {
+ const $pluginsList = $('#plugins-list');
+ $pluginsList.html('' +
+ ' ' +
+ 'Loading plugins... |
');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_get_plugins_list',
+ nonce: bpiAjax.nonce
+ },
+ success: function(response) {
+ if (response.success) {
+ renderPlugins(response.data);
+ } else {
+ $pluginsList.html('' +
+ 'Error loading plugins: ' + (response.data || 'Unknown error') +
+ ' |
');
+ }
+ },
+ error: function() {
+ $pluginsList.html('' +
+ 'Failed to load plugins. Please refresh the page and try again.' +
+ ' |
');
+ }
+ });
+ }
+
+ // Function to load installed themes
+ function loadThemes() {
+ const $themesList = $('#themes-list');
+ $themesList.html('' +
+ ' ' +
+ 'Loading themes... |
');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_get_themes_list',
+ nonce: bpiAjax.nonce
+ },
+ success: function(response) {
+ if (response.success) {
+ renderThemes(response.data);
+ } else {
+ $themesList.html('' +
+ 'Error loading themes: ' + (response.data || 'Unknown error') +
+ ' |
');
+ }
+ },
+ error: function() {
+ $themesList.html('' +
+ 'Failed to load themes. Please refresh the page and try again.' +
+ ' |
');
+ }
+ });
+ }
+
+ // Render plugins list
+ function renderPlugins(plugins) {
+ if (!plugins || plugins.length === 0) {
+ $('#plugins-list').html('No plugins found. |
');
+ return;
+ }
+
+ let html = '';
+ plugins.forEach(function(plugin) {
+ const sourceClass = plugin.source === 'repository' ? 'source-wp' :
+ (plugin.source === 'wenpai' ? 'source-wenpai' : 'source-unknown');
+ const statusClass = plugin.status === 'active' ? 'status-active' : 'status-inactive';
+ const canReinstall = plugin.source === 'repository' || plugin.source === 'wenpai';
+
+ html += `
+ |
+
+ ${escapeHtml(plugin.name)}
+ ${escapeHtml(plugin.description || '')}
+ |
+ ${escapeHtml(plugin.version)} |
+ ${escapeHtml(plugin.status)} |
+ ${escapeHtml(plugin.source)} |
+
+ ${canReinstall ?
+ `` :
+ `${bpiAjax.i18n.manual_reinstall || 'Manual reinstall required'}`
+ }
+ |
+
`;
+ });
+
+ $('#plugins-list').html(html);
+ updatePluginSelectionCount();
+ }
+
+ // Render themes list
+ function renderThemes(themes) {
+ if (!themes || themes.length === 0) {
+ $('#themes-list').html('No themes found. |
');
+ return;
+ }
+
+ let html = '';
+ themes.forEach(function(theme) {
+ const sourceClass = theme.source === 'repository' ? 'source-wp' :
+ (theme.source === 'wenpai' ? 'source-wenpai' : 'source-unknown');
+ const statusClass = theme.status === 'active' ? 'status-active' : 'status-inactive';
+ const canReinstall = theme.source === 'repository' || theme.source === 'wenpai';
+
+ html += `
+ |
+
+ ${escapeHtml(theme.name)}
+ ${escapeHtml(theme.description || '')}
+ |
+ ${escapeHtml(theme.version)} |
+ ${escapeHtml(theme.status)} |
+ ${escapeHtml(theme.source)} |
+
+ ${canReinstall ?
+ `` :
+ `${bpiAjax.i18n.manual_reinstall || 'Manual reinstall required'}`
+ }
+ |
+
`;
+ });
+
+ $('#themes-list').html(html);
+ updateThemeSelectionCount();
+ }
+
+ // Plugin selection handlers
+ $('#plugins-list').on('change', '.plugin-select', function() {
+ updatePluginSelectionCount();
+ });
+
+ $('#plugins-select-all').on('change', function() {
+ const isChecked = $(this).is(':checked');
+ $('#plugins-list .plugin-select:not(:disabled)').prop('checked', isChecked);
+ updatePluginSelectionCount();
+ });
+
+ $('#select-all-plugins').on('click', function() {
+ $('#plugins-list .plugin-select:not(:disabled)').prop('checked', true);
+ updatePluginSelectionCount();
+ });
+
+ $('#select-none-plugins').on('click', function() {
+ $('#plugins-list .plugin-select').prop('checked', false);
+ updatePluginSelectionCount();
+ });
+
+ $('#select-wp-plugins').on('click', function() {
+ $('#plugins-list tr').each(function() {
+ const source = $(this).data('source');
+ if (source === 'repository' || source === 'wenpai') {
+ $(this).find('.plugin-select').prop('checked', true);
+ } else {
+ $(this).find('.plugin-select').prop('checked', false);
+ }
+ });
+ updatePluginSelectionCount();
+ });
+
+ // Theme selection handlers
+ $('#themes-list').on('change', '.theme-select', function() {
+ updateThemeSelectionCount();
+ });
+
+ $('#themes-select-all').on('change', function() {
+ const isChecked = $(this).is(':checked');
+ $('#themes-list .theme-select:not(:disabled)').prop('checked', isChecked);
+ updateThemeSelectionCount();
+ });
+
+ $('#select-all-themes').on('click', function() {
+ $('#themes-list .theme-select:not(:disabled)').prop('checked', true);
+ updateThemeSelectionCount();
+ });
+
+ $('#select-none-themes').on('click', function() {
+ $('#themes-list .theme-select').prop('checked', false);
+ updateThemeSelectionCount();
+ });
+
+ $('#select-wp-themes').on('click', function() {
+ $('#themes-list tr').each(function() {
+ const source = $(this).data('source');
+ if (source === 'repository' || source === 'wenpai') {
+ $(this).find('.theme-select').prop('checked', true);
+ } else {
+ $(this).find('.theme-select').prop('checked', false);
+ }
+ });
+ updateThemeSelectionCount();
+ });
+
+ // Update plugin selection count and button state
+ function updatePluginSelectionCount() {
+ const selectedCount = $('#plugins-list .plugin-select:checked').length;
+ $('#reinstall-selected-plugins').prop('disabled', selectedCount === 0);
+ $('#reinstall-selected-plugins').text(
+ selectedCount > 0 ?
+ `Reinstall Selected Plugins (${selectedCount})` :
+ 'Reinstall Selected Plugins'
+ );
+ }
+
+ // Update theme selection count and button state
+ function updateThemeSelectionCount() {
+ const selectedCount = $('#themes-list .theme-select:checked').length;
+ $('#reinstall-selected-themes').prop('disabled', selectedCount === 0);
+ $('#reinstall-selected-themes').text(
+ selectedCount > 0 ?
+ `Reinstall Selected Themes (${selectedCount})` :
+ 'Reinstall Selected Themes'
+ );
+ }
+
+ // Plugin search functionality
+ $('#plugin-search').on('keyup', function() {
+ const searchTerm = $(this).val().toLowerCase();
+ $('#plugins-list tr').each(function() {
+ const pluginName = $(this).find('td:nth-child(2) strong').text().toLowerCase();
+ const pluginDesc = $(this).find('.plugin-description').text().toLowerCase();
+
+ if (pluginName.includes(searchTerm) || pluginDesc.includes(searchTerm)) {
+ $(this).show();
+ } else {
+ $(this).hide();
+ }
+ });
+ });
+
+ // Theme search functionality
+ $('#theme-search').on('keyup', function() {
+ const searchTerm = $(this).val().toLowerCase();
+ $('#themes-list tr').each(function() {
+ const themeName = $(this).find('td:nth-child(2) strong').text().toLowerCase();
+ const themeDesc = $(this).find('.theme-description').text().toLowerCase();
+
+ if (themeName.includes(searchTerm) || themeDesc.includes(searchTerm)) {
+ $(this).show();
+ } else {
+ $(this).hide();
+ }
+ });
+ });
+
+ // Plugin filters
+ $('#filter-active-plugins, #filter-inactive-plugins, #filter-wp-plugins, #filter-unknown-plugins').on('change', function() {
+ const showActive = $('#filter-active-plugins').is(':checked');
+ const showInactive = $('#filter-inactive-plugins').is(':checked');
+ const showWordPress = $('#filter-wp-plugins').is(':checked');
+ const showUnknown = $('#filter-unknown-plugins').is(':checked');
+
+ $('#plugins-list tr').each(function() {
+ const status = $(this).data('status');
+ const source = $(this).data('source');
+
+ const showByStatus = (status === 'active' && showActive) ||
+ (status === 'inactive' && showInactive);
+
+ const showBySource = (source === 'repository' && showWordPress) ||
+ (source === 'wenpai' && showWordPress) ||
+ (source !== 'repository' && source !== 'wenpai' && showUnknown);
+
+ if (showByStatus && showBySource) {
+ $(this).show();
+ } else {
+ $(this).hide();
+ }
+ });
+ });
+
+ // Theme filters
+ $('#filter-active-themes, #filter-inactive-themes, #filter-wp-themes, #filter-unknown-themes').on('change', function() {
+ const showActive = $('#filter-active-themes').is(':checked');
+ const showInactive = $('#filter-inactive-themes').is(':checked');
+ const showWordPress = $('#filter-wp-themes').is(':checked');
+ const showUnknown = $('#filter-unknown-themes').is(':checked');
+
+ $('#themes-list tr').each(function() {
+ const status = $(this).data('status');
+ const source = $(this).data('source');
+
+ const showByStatus = (status === 'active' && showActive) ||
+ (status === 'inactive' && showInactive);
+
+ const showBySource = (source === 'repository' && showWordPress) ||
+ (source === 'wenpai' && showWordPress) ||
+ (source !== 'repository' && source !== 'wenpai' && showUnknown);
+
+ if (showByStatus && showBySource) {
+ $(this).show();
+ } else {
+ $(this).hide();
+ }
+ });
+ });
+
+ // Reinstall single plugin button
+ $('#plugins-list').on('click', '.reinstall-single-plugin', function() {
+ const $row = $(this).closest('tr');
+ const plugin = {
+ slug: $row.data('slug'),
+ path: $row.data('path'),
+ source: $row.data('source')
+ };
+
+ if (!confirm(`Are you sure you want to reinstall the plugin "${$row.find('td:nth-child(2) strong').text()}"?`)) {
+ return;
+ }
+
+ reinstallPlugins([plugin]);
+ });
+
+ // Reinstall selected plugins button
+ $('#reinstall-selected-plugins').on('click', function() {
+ const selectedPlugins = [];
+ $('#plugins-list .plugin-select:checked').each(function() {
+ const $row = $(this).closest('tr');
+ selectedPlugins.push({
+ slug: $row.data('slug'),
+ path: $row.data('path'),
+ source: $row.data('source')
+ });
+ });
+
+ if (selectedPlugins.length === 0) {
+ return;
+ }
+
+ if (!confirm(`Are you sure you want to reinstall ${selectedPlugins.length} selected plugins?`)) {
+ return;
+ }
+
+ reinstallPlugins(selectedPlugins);
+ });
+
+ // Reinstall single theme button
+ $('#themes-list').on('click', '.reinstall-single-theme', function() {
+ const $row = $(this).closest('tr');
+ const theme = {
+ slug: $row.data('slug'),
+ source: $row.data('source')
+ };
+
+ if (!confirm(`Are you sure you want to reinstall the theme "${$row.find('td:nth-child(2) strong').text()}"?`)) {
+ return;
+ }
+
+ reinstallThemes([theme]);
+ });
+
+ // Reinstall selected themes button
+ $('#reinstall-selected-themes').on('click', function() {
+ const selectedThemes = [];
+ $('#themes-list .theme-select:checked').each(function() {
+ const $row = $(this).closest('tr');
+ selectedThemes.push({
+ slug: $row.data('slug'),
+ source: $row.data('source')
+ });
+ });
+
+ if (selectedThemes.length === 0) {
+ return;
+ }
+
+ if (!confirm(`Are you sure you want to reinstall ${selectedThemes.length} selected themes?`)) {
+ return;
+ }
+
+ reinstallThemes(selectedThemes);
+ });
+
+ // Function to reinstall plugins
+ function reinstallPlugins(plugins) {
+ const $results = $('#reinstall-results');
+ $results.html(`
+
Reinstalling ${plugins.length} plugins...
+
+
0/${plugins.length} ${bpiAjax.i18n.completed || 'completed'} (0% done, ${plugins.length} ${bpiAjax.i18n.remaining || 'remaining'})
+
+
`);
+
+ const $list = $results.find('.installation-list');
+ const $progress = $results.find('.progress-count');
+ const $progressBar = $results.find('.progress-bar');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_reinstall_plugins',
+ nonce: bpiAjax.nonce,
+ items: JSON.stringify(plugins)
+ },
+ success: function(response) {
+ if (response.success) {
+ const results = response.data;
+ $list.empty();
+
+ Object.keys(results).forEach(function(key) {
+ const result = results[key];
+ let itemName = key;
+
+ // Try to find the plugin name from the DOM
+ $('#plugins-list tr').each(function() {
+ if ($(this).data('path') === key || $(this).data('slug') === key) {
+ itemName = $(this).find('td:nth-child(2) strong').text();
+ }
+ });
+
+ $list.append(`
+ ${escapeHtml(itemName)}
+
+ ${result.success ? '✓ ' : '✗ '}${escapeHtml(result.message)}
+
+ `);
+ });
+
+ $progressBar.css('width', '100%').text('100%');
+ $progress.text(`${plugins.length}/${plugins.length} ${bpiAjax.i18n.completed || 'completed'} (100% done, 0 ${bpiAjax.i18n.remaining || 'remaining'})`);
+ $results.find('.notice').removeClass('notice-info').addClass('notice-success')
+ .find('p').text('Reinstallation completed! Check the results below.');
+
+ // Refresh the plugins list
+ loadPlugins();
+ } else {
+ $results.html(`
+
Error: ${escapeHtml(response.data.message || 'Unknown error')}
+
`);
+ }
+ },
+ error: function(xhr, status, error) {
+ $results.html(`
+
Ajax Error: ${escapeHtml(error || 'Unknown error')}
+
`);
+ }
+ });
+ }
+
+ // Function to reinstall themes
+ function reinstallThemes(themes) {
+ const $results = $('#reinstall-results');
+ $results.html(`
+
Reinstalling ${themes.length} themes...
+
+
0/${themes.length} ${bpiAjax.i18n.completed || 'completed'} (0% done, ${themes.length} ${bpiAjax.i18n.remaining || 'remaining'})
+
+
`);
+
+ const $list = $results.find('.installation-list');
+ const $progress = $results.find('.progress-count');
+ const $progressBar = $results.find('.progress-bar');
+
+ $.ajax({
+ url: bpiAjax.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'bpi_reinstall_themes',
+ nonce: bpiAjax.nonce,
+ items: JSON.stringify(themes)
+ },
+ success: function(response) {
+ if (response.success) {
+ const results = response.data;
+ $list.empty();
+
+ Object.keys(results).forEach(function(key) {
+ const result = results[key];
+ let itemName = key;
+
+ // Try to find the theme name from the DOM
+ $('#themes-list tr').each(function() {
+ if ($(this).data('slug') === key) {
+ itemName = $(this).find('td:nth-child(2) strong').text();
+ }
+ });
+
+ $list.append(`
+ ${escapeHtml(itemName)}
+
+ ${result.success ? '✓ ' : '✗ '}${escapeHtml(result.message)}
+
+ `);
+ });
+
+ $progressBar.css('width', '100%').text('100%');
+ $progress.text(`${themes.length}/${themes.length} ${bpiAjax.i18n.completed || 'completed'} (100% done, 0 ${bpiAjax.i18n.remaining || 'remaining'})`);
+ $results.find('.notice').removeClass('notice-info').addClass('notice-success')
+ .find('p').text('Reinstallation completed! Check the results below.');
+
+ // Refresh the themes list
+ loadThemes();
+ } else {
+ $results.html(`
+
Error: ${escapeHtml(response.data.message || 'Unknown error')}
+
`);
+ }
+ },
+ error: function(xhr, status, error) {
+ $results.html(`
+
Ajax Error: ${escapeHtml(error || 'Unknown error')}
+
`);
+ }
+ });
+ }
+ })();
+});
diff --git a/bulk-plugin-installer.php b/bulk-plugin-installer.php
index 6062a95..a7d64f5 100644
--- a/bulk-plugin-installer.php
+++ b/bulk-plugin-installer.php
@@ -2,8 +2,8 @@
/**
* 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.8
+ * Description: Bulk install WordPress plugins and themes from repository, URL, or ZIP uploads with preset collections support.
+ * Version: 1.3.0
* Author: WPMultisite.com
* Author URI: https://wpmultisite.com
* Network: true
@@ -18,52 +18,9 @@ if (!defined('WPINC')) {
die;
}
-define('BPI_VERSION', '1.1.8');
+define('BPI_VERSION', '1.2.0');
define('BPI_PATH', plugin_dir_path(__FILE__));
define('BPI_URL', plugin_dir_url(__FILE__));
-
-require_once BPI_PATH . 'includes/class-installer.php';
-require_once BPI_PATH . 'includes/admin-page.php';
-
-function bpi_init() {
- load_plugin_textdomain('bulk-plugin-installer', false, dirname(plugin_basename(__FILE__)) . '/languages/');
-
- 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',
@@ -77,16 +34,87 @@ define('BPI_TRUSTED_DOMAINS', [
'raw.githubusercontent.com'
]);
+// Include required files
+require_once BPI_PATH . 'includes/class-installer.php';
+require_once BPI_PATH . 'includes/admin-page.php';
+require_once BPI_PATH . 'includes/collections.php';
+require_once BPI_PATH . 'includes/logging.php';
+require_once BPI_PATH . 'includes/reinstaller.php';
+
+/**
+ * Initialize the plugin
+ */
+function bpi_init() {
+ load_plugin_textdomain('bulk-plugin-installer', false, dirname(plugin_basename(__FILE__)) . '/languages/');
+
+ 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');
+ }
+
+ // Register Ajax handlers
+ 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('wp_ajax_bpi_get_collection_details', 'bpi_ajax_get_collection_details');
+ add_action('wp_ajax_bpi_install_collection', 'bpi_ajax_install_collection');
+ add_action('wp_ajax_bpi_save_remote_source', 'bpi_ajax_save_remote_source');
+ add_action('wp_ajax_bpi_refresh_collections', 'bpi_ajax_refresh_collections');
+}
+add_action('plugins_loaded', 'bpi_init');
+
+/**
+ * Add menu page for single site
+ */
+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
+ );
+}
+
+/**
+ * Add menu page for multisite
+ */
+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'
+ );
+}
+
+/**
+ * Register plugin settings
+ */
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']);
+ register_setting('bpi_settings', 'bpi_collection_sources', ['sanitize_callback' => 'bpi_sanitize_collection_sources']);
+ register_setting('bpi_settings', 'bpi_install_options', ['sanitize_callback' => 'bpi_sanitize_install_options']);
}
add_action('admin_init', 'bpi_register_settings');
if (is_multisite()) {
add_action('network_admin_init', 'bpi_register_settings');
}
+/**
+ * Sanitize the roles array
+ *
+ * @param array $roles User roles
+ * @return array Sanitized roles
+ */
function bpi_sanitize_roles($roles) {
if (!is_array($roles)) {
return BPI_ALLOWED_ROLES;
@@ -95,6 +123,12 @@ function bpi_sanitize_roles($roles) {
return array_intersect($roles, $valid_roles);
}
+/**
+ * Sanitize statistics array
+ *
+ * @param array $stats Statistics data
+ * @return array Sanitized statistics
+ */
function bpi_sanitize_statistics($stats) {
return [
'total_installs' => absint($stats['total_installs'] ?? 0),
@@ -104,6 +138,60 @@ function bpi_sanitize_statistics($stats) {
];
}
+/**
+ * Sanitize collection sources
+ *
+ * @param array $sources Collection source URLs
+ * @return array Sanitized source URLs
+ */
+function bpi_sanitize_collection_sources($sources) {
+ if (!is_array($sources)) {
+ return [];
+ }
+
+ $sanitized = [];
+ foreach ($sources as $source) {
+ if (isset($source['url']) && !empty($source['url'])) {
+ $sanitized[] = [
+ 'name' => sanitize_text_field($source['name'] ?? __('Unnamed Source', 'bulk-plugin-installer')),
+ 'url' => esc_url_raw($source['url']),
+ 'enabled' => !empty($source['enabled'])
+ ];
+ }
+ }
+
+ return $sanitized;
+}
+
+/**
+ * Sanitize install options
+ *
+ * @param array $options Installation options
+ * @return array Sanitized options
+ */
+function bpi_sanitize_install_options($options) {
+ if (!is_array($options)) {
+ return [
+ 'duplicate_handling' => 'skip',
+ 'auto_activate' => false,
+ 'keep_backups' => false,
+ ];
+ }
+
+ return [
+ 'duplicate_handling' => isset($options['duplicate_handling']) && in_array($options['duplicate_handling'], ['skip', 'reinstall', 'error'])
+ ? $options['duplicate_handling']
+ : 'skip',
+ 'auto_activate' => !empty($options['auto_activate']),
+ 'keep_backups' => !empty($options['keep_backups']),
+ ];
+}
+
+/**
+ * Check if current user can install plugins
+ *
+ * @return bool True if user can install, false otherwise
+ */
function bpi_user_can_install() {
if (!is_user_logged_in()) {
return false;
@@ -116,6 +204,12 @@ function bpi_user_can_install() {
return !empty(array_intersect($allowed_roles, $user->roles)) || current_user_can('manage_options');
}
+/**
+ * Check if a domain is in the allowed list
+ *
+ * @param string $url URL to check
+ * @return bool True if domain is allowed, false otherwise
+ */
function bpi_is_domain_allowed($url) {
if (empty($url)) {
return false;
@@ -139,6 +233,9 @@ function bpi_is_domain_allowed($url) {
return false;
}
+/**
+ * Handle plugin installation AJAX request
+ */
function bpi_handle_install_plugins() {
check_ajax_referer('bpi_installer', 'nonce');
@@ -161,31 +258,7 @@ function bpi_handle_install_plugins() {
'error_code' => 400
]);
}
- $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']
- ];
- }
- }
+ $files = bpi_process_uploaded_files('plugin_files');
if (empty($files)) {
wp_send_json_error([
'message' => __('No valid files uploaded', 'bulk-plugin-installer'),
@@ -214,6 +287,44 @@ function bpi_handle_install_plugins() {
}
}
+/**
+ * Process uploaded files
+ *
+ * @param string $field_name Form field name
+ * @return array Processed files
+ */
+function bpi_process_uploaded_files($field_name) {
+ $files = [];
+ if (is_array($_FILES[$field_name]['name'])) {
+ $file_count = count($_FILES[$field_name]['name']);
+ for ($i = 0; $i < $file_count; $i++) {
+ if ($_FILES[$field_name]['error'][$i] === UPLOAD_ERR_OK) {
+ $files[] = [
+ 'name' => sanitize_file_name($_FILES[$field_name]['name'][$i]),
+ 'type' => $_FILES[$field_name]['type'][$i],
+ 'tmp_name' => $_FILES[$field_name]['tmp_name'][$i],
+ 'error' => $_FILES[$field_name]['error'][$i],
+ 'size' => $_FILES[$field_name]['size'][$i]
+ ];
+ }
+ }
+ } else {
+ if ($_FILES[$field_name]['error'] === UPLOAD_ERR_OK) {
+ $files[] = [
+ 'name' => sanitize_file_name($_FILES[$field_name]['name']),
+ 'type' => $_FILES[$field_name]['type'],
+ 'tmp_name' => $_FILES[$field_name]['tmp_name'],
+ 'error' => $_FILES[$field_name]['error'],
+ 'size' => $_FILES[$field_name]['size']
+ ];
+ }
+ }
+ return $files;
+}
+
+/**
+ * Handle theme installation AJAX request
+ */
function bpi_handle_install_themes() {
check_ajax_referer('bpi_installer', 'nonce');
@@ -236,31 +347,7 @@ function bpi_handle_install_themes() {
'error_code' => 400
]);
}
- $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']
- ];
- }
- }
+ $files = bpi_process_uploaded_files('theme_files');
if (empty($files)) {
wp_send_json_error([
'message' => __('No valid files uploaded', 'bulk-plugin-installer'),
@@ -289,6 +376,9 @@ function bpi_handle_install_themes() {
}
}
+/**
+ * Handle save settings AJAX request
+ */
function bpi_handle_save_settings() {
check_ajax_referer('bpi_installer', 'nonce');
@@ -302,12 +392,26 @@ function bpi_handle_save_settings() {
$roles = isset($_POST['bpi_allowed_roles']) ? (array)$_POST['bpi_allowed_roles'] : [];
$domains = isset($_POST['bpi_custom_domains']) ? sanitize_textarea_field($_POST['bpi_custom_domains']) : '';
+ // Save installation options
+ $install_options = isset($_POST['bpi_install_options']) ? $_POST['bpi_install_options'] : [];
+ $sanitized_options = [
+ 'duplicate_handling' => isset($install_options['duplicate_handling']) ? sanitize_text_field($install_options['duplicate_handling']) : 'skip',
+ 'auto_activate' => !empty($install_options['auto_activate']),
+ 'keep_backups' => !empty($install_options['keep_backups']),
+ ];
+ update_option('bpi_install_options', $sanitized_options);
+
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'));
}
+/**
+ * Update installation statistics
+ *
+ * @param array $results Installation results
+ */
function bpi_update_statistics($results) {
$stats = get_option('bpi_statistics', [
'total_installs' => 0,
@@ -329,12 +433,17 @@ function bpi_update_statistics($results) {
update_option('bpi_statistics', $stats);
}
+/**
+ * Plugin activation hook
+ */
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'));
}
+
+ // Check server configuration
$suggested_configs = [
'upload_max_filesize' => '64M',
'post_max_size' => '64M',
@@ -347,6 +456,8 @@ function bpi_activate() {
error_log("BPI Warning: $key is set to " . ini_get($key) . ", recommended: $value");
}
}
+
+ // Initialize statistics
if (!get_option('bpi_statistics')) {
update_option('bpi_statistics', [
'total_installs' => 0,
@@ -355,9 +466,89 @@ function bpi_activate() {
'last_install_time' => ''
]);
}
+
+ // Create default collection sources
+ if (!get_option('bpi_collection_sources')) {
+ update_option('bpi_collection_sources', [
+ [
+ 'name' => __('Official Collections', 'bulk-plugin-installer'),
+ 'url' => 'https://wpmultisite.com/api/collections.json',
+ 'enabled' => true
+ ]
+ ]);
+ }
+
+ // Create required directories
+ $collections_dir = BPI_PATH . 'data';
+ if (!file_exists($collections_dir)) {
+ wp_mkdir_p($collections_dir);
+ }
+
+ // Create backup directory
+ $backup_dir = WP_CONTENT_DIR . '/bpi-backups';
+ if (!file_exists($backup_dir)) {
+ wp_mkdir_p($backup_dir);
+ wp_mkdir_p($backup_dir . '/plugins');
+ wp_mkdir_p($backup_dir . '/themes');
+ }
+
+ // Create logs table
+ bpi_create_logs_table();
+
+ // Create default collections file if it doesn't exist
+ $collections_file = $collections_dir . '/collections.json';
+ if (!file_exists($collections_file)) {
+ $default_collections = [
+ 'version' => '1.0',
+ 'last_updated' => current_time('Y-m-d'),
+ 'collections' => [
+ 'business' => [
+ 'name' => __('Business Website', 'bulk-plugin-installer'),
+ 'slug' => 'business',
+ 'description' => __('Essential plugins for a professional business website.', 'bulk-plugin-installer'),
+ 'icon' => 'dashicons-building',
+ 'category' => 'business',
+ 'level' => 'beginner',
+ 'plugins' => [
+ 'repository' => [
+ [
+ 'slug' => 'wordpress-seo',
+ 'name' => 'Yoast SEO',
+ 'description' => __('The leading SEO plugin for WordPress', 'bulk-plugin-installer'),
+ 'required' => true
+ ],
+ [
+ 'slug' => 'contact-form-7',
+ 'name' => 'Contact Form 7',
+ 'description' => __('Simple but flexible contact form plugin', 'bulk-plugin-installer'),
+ 'required' => true
+ ]
+ ],
+ 'wenpai' => [],
+ 'url' => []
+ ],
+ 'themes' => [
+ 'repository' => [
+ [
+ 'slug' => 'astra',
+ 'name' => 'Astra',
+ 'description' => __('Fast, lightweight theme for business websites', 'bulk-plugin-installer'),
+ 'required' => false
+ ]
+ ],
+ 'wenpai' => [],
+ 'url' => []
+ ]
+ ]
+ ]
+ ];
+ file_put_contents($collections_file, wp_json_encode($default_collections, JSON_PRETTY_PRINT));
+ }
}
-// Add "Upload from URL" button to plugin install page
+/**
+ * Add "Upload from URL" button to plugin install page
+ */
function bpi_add_plugin_url_upload_button() {
if (bpi_user_can_install()) {
?>
@@ -373,7 +564,9 @@ function bpi_add_plugin_url_upload_button() {
}
add_action('admin_footer-plugin-install.php', 'bpi_add_plugin_url_upload_button');
-// Add "Upload from URL" button to theme install page
+/**
+ * Add "Upload from URL" button to theme install page
+ */
function bpi_add_theme_url_upload_button() {
if (bpi_user_can_install()) {
?>
@@ -389,6 +582,41 @@ function bpi_add_theme_url_upload_button() {
}
add_action('admin_footer-theme-install.php', 'bpi_add_theme_url_upload_button');
+/**
+ * Add "Upload from URL" button to installed plugins list page
+ */
+function bpi_add_installed_plugin_url_upload_button() {
+ if (bpi_user_can_install()) {
+ ?>
+
+
+
+ __('Installation completed! Check the results below. Failed items can be retried using the "Retry" buttons if applicable.', 'bulk-plugin-installer'),
'no_files' => __('Please select at least one ZIP file.', 'bulk-plugin-installer'),
'no_slugs' => __('Please enter at least one slug.', 'bulk-plugin-installer'),
- 'no_urls' => __('Please enter at least one URL.', 'bulk-plugin-installer')
+ 'no_urls' => __('Please enter at least one URL.', 'bulk-plugin-installer'),
+ 'confirm_install_collection' => __('Are you sure you want to install all items in this collection?', 'bulk-plugin-installer'),
+ 'installing' => __('Installing...', 'bulk-plugin-installer'),
+ 'install_collection' => __('Install Collection', 'bulk-plugin-installer'),
+ 'confirm_delete_source' => __('Are you sure you want to delete this collection source?', 'bulk-plugin-installer'),
+ 'reinstall' => __('Reinstall', 'bulk-plugin-installer'),
+ 'manual_reinstall' => __('Manual reinstall required', 'bulk-plugin-installer')
];
wp_localize_script('bpi-admin', 'bpiAjax', [
@@ -40,6 +53,10 @@ function bpi_render_admin_page() {
+
+
+
+
@@ -153,6 +170,440 @@ function bpi_render_admin_page() {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ';
+ echo '
' . __('No collections found. Please add collection sources in the settings tab or refresh.', 'bulk-plugin-installer') . '
';
+ echo '
';
+ } else {
+ foreach ($collections['collections'] as $key => $collection) :
+ $icon = isset($collection['icon']) ? $collection['icon'] : 'dashicons-admin-plugins';
+ $category = isset($collection['category']) ? $collection['category'] : '';
+ $level = isset($collection['level']) ? $collection['level'] : '';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+
+ |
+
+
+
+
+ log_time))); ?> |
+ item_name); ?> |
+ item_type)); ?> |
+ action)); ?> |
+ source)); ?> |
+
+
+ status)); ?>
+
+ |
+ message); ?> |
+
+ user_id);
+ echo $user ? esc_html($user->display_name) : __('Unknown', 'bulk-plugin-installer');
+ ?>
+ |
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
@@ -177,6 +628,45 @@ function bpi_render_admin_page() {
}
?>
+
+
+
+
+
+
+
+
+
error_messages[$code] ?? $default_message, 'bulk-plugin-installer');
+ return isset($this->error_messages[$code]) ? $this->error_messages[$code] : $default_message;
}
+ /**
+ * Check if a ZIP file is a plugin
+ *
+ * @param string $zip_path Path to ZIP file
+ * @return bool True if it's a plugin, false otherwise
+ */
private function is_plugin_zip($zip_path) {
+ if (!extension_loaded('zip')) {
+ return false;
+ }
+
$zip = new ZipArchive();
if ($zip->open($zip_path) === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
@@ -55,7 +88,17 @@ class BPI_Installer {
return false;
}
+ /**
+ * Check if a ZIP file is a theme
+ *
+ * @param string $zip_path Path to ZIP file
+ * @return bool True if it's a theme, false otherwise
+ */
private function is_theme_zip($zip_path) {
+ if (!extension_loaded('zip')) {
+ return false;
+ }
+
$zip = new ZipArchive();
if ($zip->open($zip_path) === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
@@ -69,6 +112,13 @@ class BPI_Installer {
return false;
}
+ /**
+ * Install plugins
+ *
+ * @param array $items Items to install
+ * @param string $type Installation type
+ * @return array Installation results
+ */
public function bpi_install_plugins($items, $type) {
$valid_types = ['repository', 'wenpai', 'url', 'upload'];
if (!in_array($type, $valid_types)) {
@@ -83,6 +133,13 @@ class BPI_Installer {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ // Get installation options
+ $install_options = get_option('bpi_install_options', [
+ 'duplicate_handling' => 'skip',
+ 'auto_activate' => false,
+ 'keep_backups' => false,
+ ]);
+
$results = [];
$upgrader = new Plugin_Upgrader(new WP_Ajax_Upgrader_Skin());
$installed_plugins = get_plugins();
@@ -105,12 +162,74 @@ 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'),
- 'skipped' => true
- ];
- continue;
+ switch ($install_options['duplicate_handling']) {
+ case 'skip':
+ $results[$item] = [
+ 'success' => true,
+ 'message' => __('Plugin already installed, skipped', 'bulk-plugin-installer'),
+ 'skipped' => true
+ ];
+ bpi_add_log_entry(
+ $item,
+ 'plugin',
+ 'install',
+ $type,
+ 'skipped',
+ __('Plugin already installed, skipped', 'bulk-plugin-installer')
+ );
+ continue 2; // Skip to the next item in the foreach loop
+
+ case 'reinstall':
+ // Continue with reinstall (don't add any special code here, just don't skip)
+ if ($install_options['keep_backups']) {
+ // Backup the existing plugin
+ $backup_dir = WP_CONTENT_DIR . '/bpi-backups/plugins/';
+ if (!file_exists($backup_dir)) {
+ wp_mkdir_p($backup_dir);
+ }
+
+ $plugin_dir = WP_PLUGIN_DIR . '/' . dirname($plugin_key);
+ $backup_path = $backup_dir . basename($plugin_dir) . '-' . date('Y-m-d-H-i-s') . '.zip';
+
+ // Create backup
+ if (class_exists('ZipArchive')) {
+ $zip = new ZipArchive();
+ if ($zip->open($backup_path, ZipArchive::CREATE) === TRUE) {
+ $files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($plugin_dir),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ foreach ($files as $file) {
+ if (!$file->isDir()) {
+ $filePath = $file->getRealPath();
+ $relativePath = substr($filePath, strlen($plugin_dir) + 1);
+ $zip->addFile($filePath, $relativePath);
+ }
+ }
+
+ $zip->close();
+ }
+ }
+ }
+ break;
+
+ case 'error':
+ $results[$item] = [
+ 'success' => false,
+ 'message' => __('Plugin already installed. Set "Skip duplicates" in settings to ignore this error.', 'bulk-plugin-installer'),
+ 'error_code' => 1013
+ ];
+ bpi_add_log_entry(
+ $item,
+ 'plugin',
+ 'install',
+ $type,
+ 'error',
+ __('Plugin already installed. Set "Skip duplicates" in settings to ignore this error.', 'bulk-plugin-installer')
+ );
+ continue 2; // Skip to the next item in the foreach loop
+ }
}
$download_link = '';
@@ -225,9 +344,47 @@ class BPI_Installer {
);
}
+ $success_message = __('Successfully installed', 'bulk-plugin-installer');
+
+ // Add auto-activation logic
+ if ($install_options['auto_activate'] && $result === true) {
+ $plugin_file = false;
+
+ // Find the installed plugin file
+ $plugin_folders = glob(WP_PLUGIN_DIR . '/*', GLOB_ONLYDIR);
+ foreach ($plugin_folders as $plugin_folder) {
+ $potential_main_file = basename($plugin_folder) . '.php';
+ if (file_exists($plugin_folder . '/' . $potential_main_file)) {
+ $plugin_file = basename($plugin_folder) . '/' . $potential_main_file;
+ break;
+ }
+ }
+
+ if ($plugin_file) {
+ if (is_multisite() && is_network_admin()) {
+ $activate = activate_plugin($plugin_file, '', true);
+ } else {
+ $activate = activate_plugin($plugin_file);
+ }
+
+ if (!is_wp_error($activate)) {
+ $success_message .= ' ' . __('and activated', 'bulk-plugin-installer');
+ }
+ }
+ }
+
+ bpi_add_log_entry(
+ $item,
+ 'plugin',
+ 'install',
+ $type,
+ 'success',
+ $success_message
+ );
+
$results[$item] = [
'success' => true,
- 'message' => __('Successfully installed', 'bulk-plugin-installer')
+ 'message' => $success_message
];
} catch (Exception $e) {
$no_retry_codes = [1001, 1004, 1005, 1007, 1009, 1010];
@@ -237,12 +394,28 @@ class BPI_Installer {
'error_code' => $e->getCode(),
'retry' => !in_array($e->getCode(), $no_retry_codes)
];
+
+ bpi_add_log_entry(
+ $item,
+ 'plugin',
+ 'install',
+ $type,
+ 'error',
+ $this->get_error_message($e->getCode(), $e->getMessage())
+ );
}
}
return $results;
}
+ /**
+ * Install themes
+ *
+ * @param array $items Items to install
+ * @param string $type Installation type
+ * @return array Installation results
+ */
public function bpi_install_themes($items, $type) {
$valid_types = ['repository', 'wenpai', 'url', 'upload'];
if (!in_array($type, $valid_types)) {
@@ -256,6 +429,12 @@ class BPI_Installer {
require_once ABSPATH . 'wp-admin/includes/theme-install.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+ $install_options = get_option('bpi_install_options', [
+ 'duplicate_handling' => 'skip',
+ 'auto_activate' => false,
+ 'keep_backups' => false,
+ ]);
+
$results = [];
$upgrader = new Theme_Upgrader(new WP_Ajax_Upgrader_Skin());
$installed_themes = wp_get_themes();
@@ -299,6 +478,21 @@ class BPI_Installer {
);
}
+ if (!$this->is_theme_zip($dest_path)) {
+ if ($this->is_plugin_zip($dest_path)) {
+ unlink($dest_path);
+ throw new Exception(
+ $this->get_error_message(2005, 'Plugin ZIP detected'),
+ 2005
+ );
+ }
+ unlink($dest_path);
+ throw new Exception(
+ $this->get_error_message(2006, 'Invalid theme ZIP'),
+ 2006
+ );
+ }
+
$zip = new ZipArchive();
if ($zip->open($dest_path) === true) {
$theme_key = null;
@@ -316,30 +510,80 @@ class BPI_Installer {
}
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', 'bulk-plugin-installer'),
- 'skipped' => true
- ];
- continue;
- }
+ switch ($install_options['duplicate_handling']) {
+ case 'skip':
+ if (file_exists($dest_path)) {
+ unlink($dest_path);
+ }
+ $results[$file_name] = [
+ 'success' => true,
+ 'message' => __('Theme already installed, skipped', 'bulk-plugin-installer'),
+ 'skipped' => true
+ ];
+ bpi_add_log_entry(
+ $file_name,
+ 'theme',
+ 'install',
+ $type,
+ 'skipped',
+ __('Theme already installed, skipped', 'bulk-plugin-installer')
+ );
+ continue 2; // Skip to the next item in the foreach loop
- if (!$this->is_theme_zip($dest_path)) {
- if ($this->is_plugin_zip($dest_path)) {
- unlink($dest_path);
- throw new Exception(
- $this->get_error_message(2005, 'Plugin ZIP detected'),
- 2005
- );
+ case 'reinstall':
+ // Continue with reinstall
+ if ($install_options['keep_backups']) {
+ // Backup code for themes
+ $backup_dir = WP_CONTENT_DIR . '/bpi-backups/themes/';
+ if (!file_exists($backup_dir)) {
+ wp_mkdir_p($backup_dir);
+ }
+
+ $theme_dir = get_theme_root() . '/' . $theme_key;
+ $backup_path = $backup_dir . $theme_key . '-' . date('Y-m-d-H-i-s') . '.zip';
+
+ // Create backup
+ if (class_exists('ZipArchive') && file_exists($theme_dir)) {
+ $zip = new ZipArchive();
+ if ($zip->open($backup_path, ZipArchive::CREATE) === TRUE) {
+ $files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($theme_dir),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ foreach ($files as $file) {
+ if (!$file->isDir()) {
+ $filePath = $file->getRealPath();
+ $relativePath = substr($filePath, strlen($theme_dir) + 1);
+ $zip->addFile($filePath, $relativePath);
+ }
+ }
+
+ $zip->close();
+ }
+ }
+ }
+ break;
+
+ case 'error':
+ if (file_exists($dest_path)) {
+ unlink($dest_path);
+ }
+ $results[$file_name] = [
+ 'success' => false,
+ 'message' => __('Theme already installed. Set "Skip duplicates" in settings to ignore this error.', 'bulk-plugin-installer'),
+ 'error_code' => 2013
+ ];
+ bpi_add_log_entry(
+ $file_name,
+ 'theme',
+ 'install',
+ $type,
+ 'error',
+ __('Theme already installed. Set "Skip duplicates" in settings to ignore this error.', 'bulk-plugin-installer')
+ );
+ continue 2; // Skip to the next item in the foreach loop
}
- unlink($dest_path);
- throw new Exception(
- $this->get_error_message(2006, 'Invalid theme ZIP'),
- 2006
- );
}
$download_link = $dest_path;
@@ -347,12 +591,74 @@ class BPI_Installer {
} else {
$theme_key = $item;
if ($theme_key && array_key_exists($theme_key, $installed_themes)) {
- $results[$item] = [
- 'success' => true,
- 'message' => __('Theme already installed, skipped', 'bulk-plugin-installer'),
- 'skipped' => true
- ];
- continue;
+ switch ($install_options['duplicate_handling']) {
+ case 'skip':
+ $results[$item] = [
+ 'success' => true,
+ 'message' => __('Theme already installed, skipped', 'bulk-plugin-installer'),
+ 'skipped' => true
+ ];
+ bpi_add_log_entry(
+ $item,
+ 'theme',
+ 'install',
+ $type,
+ 'skipped',
+ __('Theme already installed, skipped', 'bulk-plugin-installer')
+ );
+ continue 2; // Skip to the next item in the foreach loop
+
+ case 'reinstall':
+ // Continue with reinstall
+ if ($install_options['keep_backups']) {
+ // Backup code for themes
+ $backup_dir = WP_CONTENT_DIR . '/bpi-backups/themes/';
+ if (!file_exists($backup_dir)) {
+ wp_mkdir_p($backup_dir);
+ }
+
+ $theme_dir = get_theme_root() . '/' . $theme_key;
+ $backup_path = $backup_dir . $theme_key . '-' . date('Y-m-d-H-i-s') . '.zip';
+
+ // Create backup
+ if (class_exists('ZipArchive') && file_exists($theme_dir)) {
+ $zip = new ZipArchive();
+ if ($zip->open($backup_path, ZipArchive::CREATE) === TRUE) {
+ $files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($theme_dir),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ foreach ($files as $file) {
+ if (!$file->isDir()) {
+ $filePath = $file->getRealPath();
+ $relativePath = substr($filePath, strlen($theme_dir) + 1);
+ $zip->addFile($filePath, $relativePath);
+ }
+ }
+
+ $zip->close();
+ }
+ }
+ }
+ break;
+
+ case 'error':
+ $results[$item] = [
+ 'success' => false,
+ 'message' => __('Theme already installed. Set "Skip duplicates" in settings to ignore this error.', 'bulk-plugin-installer'),
+ 'error_code' => 2013
+ ];
+ bpi_add_log_entry(
+ $item,
+ 'theme',
+ 'install',
+ $type,
+ 'error',
+ __('Theme already installed. Set "Skip duplicates" in settings to ignore this error.', 'bulk-plugin-installer')
+ );
+ continue 2; // Skip to the next item in the foreach loop
+ }
}
}
@@ -426,6 +732,15 @@ class BPI_Installer {
);
}
+ bpi_add_log_entry(
+ $item,
+ 'theme',
+ 'install',
+ $type,
+ 'success',
+ __('Successfully installed', 'bulk-plugin-installer')
+ );
+
$results[$item] = [
'success' => true,
'message' => __('Successfully installed', 'bulk-plugin-installer')
@@ -438,9 +753,18 @@ class BPI_Installer {
'error_code' => $e->getCode(),
'retry' => !in_array($e->getCode(), $no_retry_codes)
];
+
+ bpi_add_log_entry(
+ $item,
+ 'theme',
+ 'install',
+ $type,
+ 'error',
+ $this->get_error_message($e->getCode(), $e->getMessage())
+ );
}
}
return $results;
}
-}
\ No newline at end of file
+}
diff --git a/includes/collections.php b/includes/collections.php
new file mode 100644
index 0000000..25bb349
--- /dev/null
+++ b/includes/collections.php
@@ -0,0 +1,489 @@
+ '1.0',
+ 'last_updated' => current_time('Y-m-d'),
+ 'collections' => []
+ ];
+
+ // First try to load from remote sources
+ $sources = get_option('bpi_collection_sources', []);
+ $loaded_remote = false;
+
+ foreach ($sources as $source) {
+ if (empty($source['enabled']) || empty($source['url'])) {
+ continue;
+ }
+
+ $response = wp_remote_get($source['url'], [
+ 'timeout' => 15,
+ 'sslverify' => true,
+ 'headers' => [
+ 'User-Agent' => 'WordPress/Bulk-Plugin-Installer-' . BPI_VERSION
+ ]
+ ]);
+
+ if (!is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response)) {
+ $remote_data = json_decode(wp_remote_retrieve_body($response), true);
+
+ if (is_array($remote_data) && !empty($remote_data['collections'])) {
+ if (isset($remote_data['version']) && version_compare($remote_data['version'], $collections['version'], '>')) {
+ $collections['version'] = $remote_data['version'];
+ }
+
+ if (isset($remote_data['last_updated'])) {
+ $collections['last_updated'] = $remote_data['last_updated'];
+ }
+
+ $collections['collections'] = array_merge($collections['collections'], $remote_data['collections']);
+ $loaded_remote = true;
+ }
+ }
+ }
+
+ // If no remote collections were loaded, try local file
+ if (!$loaded_remote) {
+ $local_file = BPI_PATH . 'data/collections.json';
+ if (file_exists($local_file)) {
+ $local_data = json_decode(file_get_contents($local_file), true);
+ if (is_array($local_data) && !empty($local_data['collections'])) {
+ $collections = $local_data;
+ }
+ }
+ }
+
+ // Cache for 24 hours
+ set_transient('bpi_preset_collections', $collections, DAY_IN_SECONDS);
+ }
+
+ return apply_filters('bpi_preset_collections', $collections);
+}
+
+/**
+ * Count items in a collection
+ *
+ * @param array $items Collection items
+ * @return int Total count
+ */
+function bpi_count_collection_items($items) {
+ $count = 0;
+
+ if (!is_array($items)) {
+ return 0;
+ }
+
+ foreach ($items as $source => $source_items) {
+ if (is_array($source_items)) {
+ $count += count($source_items);
+ }
+ }
+
+ return $count;
+}
+
+/**
+ * Get AJAX handler for collection details
+ */
+function bpi_ajax_get_collection_details() {
+ check_ajax_referer('bpi_installer', 'nonce');
+
+ if (!bpi_user_can_install()) {
+ wp_send_json_error([
+ 'message' => __('Insufficient permissions', 'bulk-plugin-installer')
+ ]);
+ }
+
+ $collection_id = sanitize_text_field($_POST['collection_id'] ?? '');
+ $collections = bpi_get_preset_collections();
+
+ if (!isset($collections['collections'][$collection_id])) {
+ wp_send_json_error([
+ 'message' => __('Collection not found', 'bulk-plugin-installer')
+ ]);
+ }
+
+ $collection = $collections['collections'][$collection_id];
+
+ // Generate HTML output
+ ob_start();
+ ?>
+
+
+
+
+
+
![<?php echo esc_attr($collection['name']); ?>](<?php echo esc_url($collection['screenshot']); ?>)
+
+
+
+
+
+
+
+
+
+ $plugins) : ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $themes) : ?>
+
+
+
+
+
+
+
+
+ $html,
+ 'name' => $collection['name']
+ ]);
+}
+
+/**
+ * Install collection AJAX handler
+ */
+function bpi_ajax_install_collection() {
+ check_ajax_referer('bpi_installer', 'nonce');
+
+ if (!bpi_user_can_install()) {
+ wp_send_json_error([
+ 'message' => __('Insufficient permissions', 'bulk-plugin-installer')
+ ]);
+ }
+
+ $collection_id = sanitize_text_field($_POST['collection_id'] ?? '');
+ $collections = bpi_get_preset_collections();
+
+ if (!isset($collections['collections'][$collection_id])) {
+ wp_send_json_error([
+ 'message' => __('Collection not found', 'bulk-plugin-installer')
+ ]);
+ }
+
+ $collection = $collections['collections'][$collection_id];
+ $installer = new BPI_Installer();
+ $results = [
+ 'plugins' => [],
+ 'themes' => []
+ ];
+
+ // Install plugins
+ if (!empty($collection['plugins']) && is_array($collection['plugins'])) {
+ foreach ($collection['plugins'] as $source => $plugins) {
+ if (!is_array($plugins) || empty($plugins)) {
+ continue;
+ }
+
+ $items = [];
+ foreach ($plugins as $plugin) {
+ $items[] = is_array($plugin) ? $plugin['slug'] : $plugin;
+ }
+
+ if (!empty($items)) {
+ $source_results = $installer->bpi_install_plugins($items, $source);
+ $results['plugins'] = array_merge($results['plugins'], $source_results);
+ }
+ }
+ }
+
+ // Install themes
+ if (!empty($collection['themes']) && is_array($collection['themes'])) {
+ foreach ($collection['themes'] as $source => $themes) {
+ if (!is_array($themes) || empty($themes)) {
+ continue;
+ }
+
+ $items = [];
+ foreach ($themes as $theme) {
+ $items[] = is_array($theme) ? $theme['slug'] : $theme;
+ }
+
+ if (!empty($items)) {
+ $source_results = $installer->bpi_install_themes($items, $source);
+ $results['themes'] = array_merge($results['themes'], $source_results);
+ }
+ }
+ }
+
+ // Update statistics
+ bpi_update_statistics(array_merge($results['plugins'], $results['themes']));
+
+ // Generate HTML output
+ ob_start();
+ ?>
+
+
+
+
+
+
+
+ $result) : ?>
+ -
+
+
+
+
+
+
+ ✗
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $result) : ?>
+ -
+
+
+
+
+
+
+ ✗
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $html,
+ 'results' => $results
+ ]);
+}
+
+/**
+ * Save remote collection source
+ */
+function bpi_ajax_save_remote_source() {
+ check_ajax_referer('bpi_installer', 'nonce');
+
+ if (!current_user_can('manage_options')) {
+ wp_send_json_error([
+ 'message' => __('Insufficient permissions', 'bulk-plugin-installer')
+ ]);
+ }
+
+ $name = sanitize_text_field($_POST['name'] ?? '');
+ $url = esc_url_raw($_POST['url'] ?? '');
+ $id = sanitize_text_field($_POST['id'] ?? '');
+
+ if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
+ wp_send_json_error([
+ 'message' => __('Please enter a valid URL', 'bulk-plugin-installer')
+ ]);
+ }
+
+ $sources = get_option('bpi_collection_sources', []);
+
+ // Check if URL is valid and accessible
+ $response = wp_remote_get($url, [
+ 'timeout' => 15,
+ 'sslverify' => true,
+ 'headers' => [
+ 'User-Agent' => 'WordPress/Bulk-Plugin-Installer-' . BPI_VERSION
+ ]
+ ]);
+
+ if (is_wp_error($response)) {
+ wp_send_json_error([
+ 'message' => __('Unable to access URL. Error: ', 'bulk-plugin-installer') . $response->get_error_message()
+ ]);
+ }
+
+ $response_code = wp_remote_retrieve_response_code($response);
+ if ($response_code !== 200) {
+ wp_send_json_error([
+ 'message' => sprintf(__('Invalid response from URL. Status code: %d', 'bulk-plugin-installer'), $response_code)
+ ]);
+ }
+
+ $json = json_decode(wp_remote_retrieve_body($response), true);
+ if (!is_array($json) || !isset($json['collections']) || !is_array($json['collections'])) {
+ wp_send_json_error([
+ 'message' => __('The URL does not contain valid collections data', 'bulk-plugin-installer')
+ ]);
+ }
+
+ if (empty($id)) {
+ // Add new source
+ $sources[] = [
+ 'name' => !empty($name) ? $name : sprintf(__('Collection Source %d', 'bulk-plugin-installer'), count($sources) + 1),
+ 'url' => $url,
+ 'enabled' => true
+ ];
+ } else {
+ // Update existing source
+ foreach ($sources as $index => $source) {
+ if ($index == $id) {
+ $sources[$index]['name'] = !empty($name) ? $name : $source['name'];
+ $sources[$index]['url'] = $url;
+ break;
+ }
+ }
+ }
+
+ update_option('bpi_collection_sources', $sources);
+ delete_transient('bpi_preset_collections');
+
+ wp_send_json_success([
+ 'message' => __('Collection source saved successfully', 'bulk-plugin-installer'),
+ 'sources' => $sources
+ ]);
+}
+
+/**
+ * Refresh collections
+ */
+function bpi_ajax_refresh_collections() {
+ check_ajax_referer('bpi_installer', 'nonce');
+
+ if (!current_user_can('manage_options')) {
+ wp_send_json_error([
+ 'message' => __('Insufficient permissions', 'bulk-plugin-installer')
+ ]);
+ }
+
+ delete_transient('bpi_preset_collections');
+ $collections = bpi_get_preset_collections(true);
+
+ wp_send_json_success([
+ 'message' => __('Collections refreshed successfully', 'bulk-plugin-installer'),
+ 'count' => count($collections['collections']),
+ 'last_updated' => $collections['last_updated']
+ ]);
+}
diff --git a/includes/logging.php b/includes/logging.php
new file mode 100644
index 0000000..f1b73c7
--- /dev/null
+++ b/includes/logging.php
@@ -0,0 +1,275 @@
+prefix . 'bpi_logs';
+ $charset_collate = $wpdb->get_charset_collate();
+
+ if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
+ $sql = "CREATE TABLE $table_name (
+ id bigint(20) NOT NULL AUTO_INCREMENT,
+ log_time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
+ item_name varchar(255) NOT NULL,
+ item_type varchar(50) NOT NULL,
+ action varchar(50) NOT NULL,
+ source varchar(100) NOT NULL,
+ status varchar(50) NOT NULL,
+ message text NOT NULL,
+ user_id bigint(20) NOT NULL,
+ PRIMARY KEY (id),
+ KEY log_time (log_time),
+ KEY item_type (item_type),
+ KEY status (status)
+ ) $charset_collate;";
+
+ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+ dbDelta($sql);
+ }
+}
+
+/**
+ * Add log entry
+ *
+ * @param string $item_name Item name
+ * @param string $item_type Item type (plugin or theme)
+ * @param string $action Action (install, update, delete)
+ * @param string $source Source (repository, wenpai, url, upload)
+ * @param string $status Status (success, error, skipped)
+ * @param string $message Message
+ * @return int|false The log ID or false on failure
+ */
+function bpi_add_log_entry($item_name, $item_type, $action, $source, $status, $message) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'bpi_logs';
+
+ return $wpdb->insert(
+ $table_name,
+ [
+ 'log_time' => current_time('mysql'),
+ 'item_name' => $item_name,
+ 'item_type' => $item_type,
+ 'action' => $action,
+ 'source' => $source,
+ 'status' => $status,
+ 'message' => $message,
+ 'user_id' => get_current_user_id()
+ ],
+ [
+ '%s', // log_time
+ '%s', // item_name
+ '%s', // item_type
+ '%s', // action
+ '%s', // source
+ '%s', // status
+ '%s', // message
+ '%d' // user_id
+ ]
+ );
+}
+
+/**
+ * Get logs
+ *
+ * @param array $filters Filters
+ * @param int $per_page Items per page
+ * @param int $page Current page
+ * @return array Logs data
+ */
+function bpi_get_logs($filters = [], $per_page = 20, $page = 1) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'bpi_logs';
+
+ $where = [];
+ $prepare = [];
+
+ if (!empty($filters['item_type'])) {
+ $where[] = 'item_type = %s';
+ $prepare[] = $filters['item_type'];
+ }
+
+ if (!empty($filters['status'])) {
+ $where[] = 'status = %s';
+ $prepare[] = $filters['status'];
+ }
+
+ if (!empty($filters['date_from'])) {
+ $where[] = 'log_time >= %s';
+ $prepare[] = $filters['date_from'] . ' 00:00:00';
+ }
+
+ if (!empty($filters['date_to'])) {
+ $where[] = 'log_time <= %s';
+ $prepare[] = $filters['date_to'] . ' 23:59:59';
+ }
+
+ if (!empty($filters['search'])) {
+ $where[] = '(item_name LIKE %s OR message LIKE %s)';
+ $search_term = '%' . $wpdb->esc_like($filters['search']) . '%';
+ $prepare[] = $search_term;
+ $prepare[] = $search_term;
+ }
+
+ $where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
+
+ $offset = ($page - 1) * $per_page;
+
+ $query = $wpdb->prepare(
+ "SELECT * FROM $table_name $where_clause ORDER BY log_time DESC LIMIT %d OFFSET %d",
+ array_merge($prepare, [$per_page, $offset])
+ );
+
+ $logs = $wpdb->get_results($query);
+
+ // Get total count for pagination
+ $count_query = $wpdb->prepare(
+ "SELECT COUNT(*) FROM $table_name $where_clause",
+ $prepare
+ );
+
+ $total = $wpdb->get_var($count_query);
+
+ return [
+ 'logs' => $logs,
+ 'total' => $total,
+ 'pages' => ceil($total / $per_page)
+ ];
+}
+
+/**
+ * Clear logs
+ */
+function bpi_clear_logs() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'bpi_logs';
+
+ $wpdb->query("TRUNCATE TABLE $table_name");
+}
+
+/**
+ * Handle clear logs request
+ */
+function bpi_handle_clear_logs() {
+ if (isset($_POST['action']) && $_POST['action'] === 'bpi_clear_logs' && isset($_POST['bpi_logs_nonce']) && wp_verify_nonce($_POST['bpi_logs_nonce'], 'bpi_clear_logs')) {
+ if (!current_user_can('manage_options')) {
+ wp_die(__('You do not have sufficient permissions to access this page.', 'bulk-plugin-installer'));
+ }
+
+ bpi_clear_logs();
+
+ wp_redirect(add_query_arg(['page' => 'bulk-plugin-installer', 'tab' => 'logs', 'cleared' => '1'], admin_url('plugins.php')));
+ exit;
+ }
+}
+add_action('admin_init', 'bpi_handle_clear_logs');
+
+/**
+ * Handle export logs request
+ */
+function bpi_handle_export_logs() {
+ if (isset($_GET['action']) && $_GET['action'] === 'bpi_export_logs' && isset($_GET['bpi_logs_nonce']) && wp_verify_nonce($_GET['bpi_logs_nonce'], 'bpi_export_logs')) {
+ if (!current_user_can('manage_options')) {
+ wp_die(__('You do not have sufficient permissions to access this page.', 'bulk-plugin-installer'));
+ }
+
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'bpi_logs';
+
+ $logs = $wpdb->get_results("SELECT * FROM $table_name ORDER BY log_time DESC");
+
+ // Set headers for CSV download
+ header('Content-Type: text/csv; charset=utf-8');
+ header('Content-Disposition: attachment; filename=bpi-logs-' . date('Y-m-d') . '.csv');
+
+ // Create output stream
+ $output = fopen('php://output', 'w');
+
+ // Add BOM for UTF-8
+ fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
+
+ // Add headers
+ fputcsv($output, [
+ __('ID', 'bulk-plugin-installer'),
+ __('Time', 'bulk-plugin-installer'),
+ __('Item', 'bulk-plugin-installer'),
+ __('Type', 'bulk-plugin-installer'),
+ __('Action', 'bulk-plugin-installer'),
+ __('Source', 'bulk-plugin-installer'),
+ __('Status', 'bulk-plugin-installer'),
+ __('Message', 'bulk-plugin-installer'),
+ __('User ID', 'bulk-plugin-installer'),
+ __('Username', 'bulk-plugin-installer'),
+ ]);
+
+ // Add data rows
+ foreach ($logs as $log) {
+ $user = get_userdata($log->user_id);
+ $username = $user ? $user->user_login : __('Unknown', 'bulk-plugin-installer');
+
+ fputcsv($output, [
+ $log->id,
+ $log->log_time,
+ $log->item_name,
+ $log->item_type,
+ $log->action,
+ $log->source,
+ $log->status,
+ $log->message,
+ $log->user_id,
+ $username
+ ]);
+ }
+
+ fclose($output);
+ exit;
+ }
+}
+add_action('admin_post_bpi_export_logs', 'bpi_handle_export_logs');
+
+/**
+ * Log WordPress core plugin/theme installations
+ */
+function bpi_log_wp_installations() {
+ add_action('upgrader_process_complete', function($upgrader, $hook_extra) {
+ if (isset($hook_extra['type']) && isset($hook_extra['action'])) {
+ // For plugins
+ if ($hook_extra['type'] === 'plugin' && $hook_extra['action'] === 'install') {
+ $plugin_info = $upgrader->plugin_info();
+ if ($plugin_info) {
+ $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_info);
+ $plugin_name = $plugin_data['Name'] ?? basename($plugin_info, '.php');
+ bpi_add_log_entry(
+ $plugin_name,
+ 'plugin',
+ 'install',
+ 'wordpress_core',
+ 'success',
+ __('Plugin installed via WordPress core installer', 'bulk-plugin-installer')
+ );
+ }
+ }
+
+ // For themes
+ if ($hook_extra['type'] === 'theme' && $hook_extra['action'] === 'install') {
+ $theme_info = $upgrader->theme_info();
+ if ($theme_info) {
+ $theme_name = $theme_info->get('Name');
+ bpi_add_log_entry(
+ $theme_name,
+ 'theme',
+ 'install',
+ 'wordpress_core',
+ 'success',
+ __('Theme installed via WordPress core installer', 'bulk-plugin-installer')
+ );
+ }
+ }
+ }
+ }, 10, 2);
+}
+add_action('init', 'bpi_log_wp_installations');
\ No newline at end of file
diff --git a/includes/reinstaller.php b/includes/reinstaller.php
new file mode 100644
index 0000000..1ef9ee9
--- /dev/null
+++ b/includes/reinstaller.php
@@ -0,0 +1,451 @@
+ $plugin_data) {
+ $plugin_slug = dirname($plugin_path);
+ if ($plugin_slug === '.') {
+ // Handle single-file plugins
+ $plugin_slug = basename($plugin_path, '.php');
+ }
+
+ // Default source
+ $source = 'unknown';
+
+ // Get plugin status
+ if (is_plugin_active($plugin_path)) {
+ $status = 'active';
+ } else {
+ $status = 'inactive';
+ }
+
+ // Source determination logic
+ // 1. Check common WordPress.org plugins first
+ $common_wp_plugins = [
+ 'akismet', 'hello', 'contact-form-7', 'woocommerce', 'wordpress-seo',
+ 'jetpack', 'wordfence', 'elementor', 'all-in-one-seo-pack', 'wp-super-cache'
+ ];
+
+ if (in_array($plugin_slug, $common_wp_plugins)) {
+ $source = 'repository';
+ } else {
+ // 2. Check plugin/author URI for wordpress.org references
+ if (
+ strpos($plugin_data['PluginURI'], 'wordpress.org') !== false ||
+ strpos($plugin_data['AuthorURI'], 'wordpress.org') !== false
+ ) {
+ $source = 'repository';
+ }
+ // 3. Check for WenPai sources
+ elseif (
+ strpos($plugin_data['PluginURI'], 'wenpai.net') !== false ||
+ strpos($plugin_data['PluginURI'], 'wenpai.org') !== false ||
+ strpos($plugin_data['PluginURI'], 'wenpai.cn') !== false ||
+ strpos($plugin_data['AuthorURI'], 'wenpai.net') !== false ||
+ strpos($plugin_data['AuthorURI'], 'wenpai.org') !== false ||
+ strpos($plugin_data['AuthorURI'], 'wenpai.cn') !== false
+ ) {
+ $source = 'wenpai';
+ }
+ // 4. Check if plugin exists in WordPress.org repository
+ elseif (!isset($repository_check_cache[$plugin_slug])) {
+ $repository_check_cache[$plugin_slug] = bpi_plugin_exists_in_repository($plugin_slug);
+ if ($repository_check_cache[$plugin_slug]) {
+ $source = 'repository';
+ }
+ } elseif ($repository_check_cache[$plugin_slug]) {
+ $source = 'repository';
+ }
+ }
+
+ $installed_plugins[] = [
+ 'name' => $plugin_data['Name'],
+ 'slug' => $plugin_slug,
+ 'path' => $plugin_path,
+ 'version' => $plugin_data['Version'],
+ 'source' => $source,
+ 'status' => $status,
+ 'plugin_uri' => $plugin_data['PluginURI'],
+ 'author' => $plugin_data['Author'],
+ 'author_uri' => $plugin_data['AuthorURI'],
+ 'description' => $plugin_data['Description']
+ ];
+ }
+
+ return $installed_plugins;
+}
+
+/**
+ * Get all installed themes
+ *
+ * @return array Installed themes information
+ */
+function bpi_get_installed_themes() {
+ $all_themes = wp_get_themes();
+ $installed_themes = [];
+ $active_theme = wp_get_theme();
+
+ // Create a cache for repository existence checks
+ $repository_check_cache = [];
+
+ foreach ($all_themes as $theme_slug => $theme_obj) {
+ // Default source
+ $source = 'unknown';
+
+ // Check if theme is active
+ if ($active_theme->get_stylesheet() === $theme_slug) {
+ $status = 'active';
+ } else {
+ $status = 'inactive';
+ }
+
+ // Get theme URIs
+ $theme_uri = $theme_obj->get('ThemeURI');
+ $author_uri = $theme_obj->get('AuthorURI');
+
+ // Source determination logic
+ // 1. Check common WordPress.org themes first
+ $common_wp_themes = [
+ 'twentytwentyfour', 'twentytwentythree', 'twentytwentytwo', 'twentytwentyone',
+ 'twentytwenty', 'twentynineteen', 'twentyeighteen', 'twentyseventeen'
+ ];
+
+ if (in_array($theme_slug, $common_wp_themes)) {
+ $source = 'repository';
+ } else {
+ // 2. Check theme/author URI for wordpress.org references
+ if (
+ strpos($theme_uri, 'wordpress.org') !== false ||
+ strpos($author_uri, 'wordpress.org') !== false
+ ) {
+ $source = 'repository';
+ }
+ // 3. Check for WenPai sources
+ elseif (
+ strpos($theme_uri, 'wenpai.net') !== false ||
+ strpos($theme_uri, 'wenpai.org') !== false ||
+ strpos($theme_uri, 'wenpai.cn') !== false ||
+ strpos($author_uri, 'wenpai.net') !== false ||
+ strpos($author_uri, 'wenpai.org') !== false ||
+ strpos($author_uri, 'wenpai.cn') !== false
+ ) {
+ $source = 'wenpai';
+ }
+ // 4. Check if theme exists in WordPress.org repository
+ elseif (!isset($repository_check_cache[$theme_slug])) {
+ $repository_check_cache[$theme_slug] = bpi_theme_exists_in_repository($theme_slug);
+ if ($repository_check_cache[$theme_slug]) {
+ $source = 'repository';
+ }
+ } elseif ($repository_check_cache[$theme_slug]) {
+ $source = 'repository';
+ }
+ }
+
+ $installed_themes[] = [
+ 'name' => $theme_obj->get('Name'),
+ 'slug' => $theme_slug,
+ 'version' => $theme_obj->get('Version'),
+ 'source' => $source,
+ 'status' => $status,
+ 'theme_uri' => $theme_uri,
+ 'author' => $theme_obj->get('Author'),
+ 'author_uri' => $author_uri,
+ 'description' => $theme_obj->get('Description')
+ ];
+ }
+
+ return $installed_themes;
+}
+
+/**
+ * AJAX handler for getting installed plugins
+ */
+function bpi_ajax_get_plugins_list() {
+ 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'));
+ }
+
+ $installed_plugins = bpi_get_installed_plugins();
+ wp_send_json_success($installed_plugins);
+}
+add_action('wp_ajax_bpi_get_plugins_list', 'bpi_ajax_get_plugins_list');
+
+/**
+ * AJAX handler for getting installed themes
+ */
+function bpi_ajax_get_themes_list() {
+ 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'));
+ }
+
+ $installed_themes = bpi_get_installed_themes();
+ wp_send_json_success($installed_themes);
+}
+add_action('wp_ajax_bpi_get_themes_list', 'bpi_ajax_get_themes_list');
+
+/**
+ * AJAX handler for reinstalling plugins
+ */
+function bpi_handle_reinstall_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([
+ 'message' => __('Insufficient permissions', 'bulk-plugin-installer'),
+ 'error_code' => 403
+ ]);
+ }
+
+ $items = isset($_POST['items']) ? json_decode(stripslashes($_POST['items']), true) : [];
+ if (!is_array($items) || empty($items)) {
+ wp_send_json_error([
+ 'message' => __('No items provided', 'bulk-plugin-installer'),
+ 'error_code' => 400
+ ]);
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+ require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
+
+ $results = [];
+ $installer = new BPI_Installer();
+
+ foreach ($items as $item) {
+ $source = sanitize_text_field($item['source'] ?? 'repository');
+ $slug = sanitize_text_field($item['slug'] ?? '');
+ $path = sanitize_text_field($item['path'] ?? '');
+ $was_active = is_plugin_active($path);
+
+ if (empty($slug)) {
+ $results[$path] = [
+ 'success' => false,
+ 'message' => __('Invalid plugin slug', 'bulk-plugin-installer'),
+ 'error_code' => 400
+ ];
+ continue;
+ }
+
+ // Deactivate plugin first
+ if ($was_active) {
+ deactivate_plugins($path);
+ }
+
+ // For repository or wenpai source, use reinstaller
+ if ($source === 'repository' || $source === 'wenpai') {
+ // Use the existing installer method
+ $source_results = $installer->bpi_install_plugins([$slug], $source);
+
+ // Get the result for this specific item
+ if (isset($source_results[$slug])) {
+ $results[$path] = $source_results[$slug];
+
+ // If successful and the plugin was active before, try to reactivate
+ if ($was_active && $source_results[$slug]['success']) {
+ $plugin_file = WP_PLUGIN_DIR . '/' . $path;
+ if (file_exists($plugin_file)) {
+ activate_plugin($path);
+ $results[$path]['message'] .= ' ' . __('and reactivated', 'bulk-plugin-installer');
+ }
+ }
+ } else {
+ $results[$path] = [
+ 'success' => false,
+ 'message' => __('Failed to reinstall plugin', 'bulk-plugin-installer'),
+ 'error_code' => 500
+ ];
+ }
+ } else {
+ // For unknown or custom sources
+ $results[$path] = [
+ 'success' => false,
+ 'message' => __('Cannot reinstall plugin from unknown source. Please reinstall manually.', 'bulk-plugin-installer'),
+ 'error_code' => 400,
+ 'source' => $source
+ ];
+ }
+
+ // Log the reinstallation
+ bpi_add_log_entry(
+ $slug,
+ 'plugin',
+ 'reinstall',
+ $source,
+ $results[$path]['success'] ? 'success' : 'error',
+ $results[$path]['message']
+ );
+ }
+
+ bpi_update_statistics($results);
+ wp_send_json_success($results);
+}
+add_action('wp_ajax_bpi_reinstall_plugins', 'bpi_handle_reinstall_plugins');
+
+/**
+ * AJAX handler for reinstalling themes
+ */
+function bpi_handle_reinstall_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([
+ 'message' => __('Insufficient permissions', 'bulk-plugin-installer'),
+ 'error_code' => 403
+ ]);
+ }
+
+ $items = isset($_POST['items']) ? json_decode(stripslashes($_POST['items']), true) : [];
+ if (!is_array($items) || empty($items)) {
+ wp_send_json_error([
+ 'message' => __('No items provided', 'bulk-plugin-installer'),
+ 'error_code' => 400
+ ]);
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+ require_once ABSPATH . 'wp-admin/includes/theme-install.php';
+
+ $results = [];
+ $installer = new BPI_Installer();
+ $active_theme = wp_get_theme();
+
+ foreach ($items as $item) {
+ $source = sanitize_text_field($item['source'] ?? 'repository');
+ $slug = sanitize_text_field($item['slug'] ?? '');
+ $was_active = ($active_theme->get_stylesheet() === $slug);
+
+ if (empty($slug)) {
+ $results[$slug] = [
+ 'success' => false,
+ 'message' => __('Invalid theme slug', 'bulk-plugin-installer'),
+ 'error_code' => 400
+ ];
+ continue;
+ }
+
+ // For repository or wenpai source, use reinstaller
+ if ($source === 'repository' || $source === 'wenpai') {
+ // Use the existing installer method
+ $source_results = $installer->bpi_install_themes([$slug], $source);
+
+ // Get the result for this specific item
+ if (isset($source_results[$slug])) {
+ $results[$slug] = $source_results[$slug];
+
+ // If successful and the theme was active before, try to reactivate
+ if ($was_active && $source_results[$slug]['success']) {
+ switch_theme($slug);
+ $results[$slug]['message'] .= ' ' . __('and reactivated', 'bulk-plugin-installer');
+ }
+ } else {
+ $results[$slug] = [
+ 'success' => false,
+ 'message' => __('Failed to reinstall theme', 'bulk-plugin-installer'),
+ 'error_code' => 500
+ ];
+ }
+ } else {
+ // For unknown or custom sources
+ $results[$slug] = [
+ 'success' => false,
+ 'message' => __('Cannot reinstall theme from unknown source. Please reinstall manually.', 'bulk-plugin-installer'),
+ 'error_code' => 400,
+ 'source' => $source
+ ];
+ }
+
+ // Log the reinstallation
+ bpi_add_log_entry(
+ $slug,
+ 'theme',
+ 'reinstall',
+ $source,
+ $results[$slug]['success'] ? 'success' : 'error',
+ $results[$slug]['message']
+ );
+ }
+
+ bpi_update_statistics($results);
+ wp_send_json_success($results);
+}
+add_action('wp_ajax_bpi_reinstall_themes', 'bpi_handle_reinstall_themes');