From 93d19b6da0a8a6c48c93553d5620fe95eb406961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E6=B4=BE=E5=A4=87=E6=A1=88?= <130886204+modiqi@users.noreply.github.com> Date: Tue, 20 May 2025 00:39:06 +0800 Subject: [PATCH] =?UTF-8?q?dev=20=E6=B7=BB=E5=8A=A0=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E5=90=88=E9=9B=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/admin.css | 573 +++++++++++++++++++++- assets/js/admin.js | 902 ++++++++++++++++++++++++++++++++++- bulk-plugin-installer.php | 424 ++++++++++++---- data/collections.json | 44 ++ includes/admin-page.php | 494 ++++++++++++++++++- includes/class-installer.php | 402 ++++++++++++++-- includes/collections.php | 489 +++++++++++++++++++ includes/logging.php | 275 +++++++++++ includes/reinstaller.php | 451 ++++++++++++++++++ 9 files changed, 3884 insertions(+), 170 deletions(-) create mode 100644 data/collections.json create mode 100644 includes/collections.php create mode 100644 includes/logging.php create mode 100644 includes/reinstaller.php 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%
+
+
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%
    +
    +
    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%
    +
    +
    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'] : ''; + ?> +
    +
    + +
    +

    +

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

    +

    + + +

    + + + + + + + + + + + + '; + } else { + foreach ($sources as $index => $source) { + ?> + + + + + + + + +
    ' . __('No sources added yet. Add a source below.', 'bulk-plugin-installer') . '
    + + + + +
    + +
    +

    +
    + +
    + + +
    +
    + + +

    + +

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

    +

    + + +
    +

    +
    + + +
    +
    + + + +
    + + + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    log_time))); ?>item_name); ?>item_type)); ?>action)); ?>source)); ?> + + status)); ?> + + message); ?> + user_id); + echo $user ? esc_html($user->display_name) : __('Unknown', 'bulk-plugin-installer'); + ?> +
    + + 1): ?> +
    +
    + + + + + add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => '«', + 'next_text' => '»', + 'total' => $total_pages, + 'current' => $current_page + ]); + ?> + +
    +
    + + +
    +
    + + + +
    + + + + +
    +
    +

    @@ -177,6 +628,45 @@ function bpi_render_admin_page() { } ?>
    + +
    +

    + 'skip', + 'auto_activate' => false, + 'keep_backups' => false, + ]); + ?> + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +