Changeset 3448097
- Timestamp:
- 01/27/2026 05:30:57 PM (4 weeks ago)
- Location:
- blaminhor-essentials
- Files:
-
- 95 added
- 9 edited
-
tags/1.3.1 (added)
-
tags/1.3.1/assets (added)
-
tags/1.3.1/assets/css (added)
-
tags/1.3.1/assets/css/admin.css (added)
-
tags/1.3.1/assets/css/admin.min.css (added)
-
tags/1.3.1/assets/css/index.php (added)
-
tags/1.3.1/assets/css/modules.css (added)
-
tags/1.3.1/assets/images (added)
-
tags/1.3.1/assets/index.php (added)
-
tags/1.3.1/assets/js (added)
-
tags/1.3.1/assets/js/admin.js (added)
-
tags/1.3.1/assets/js/admin.min.js (added)
-
tags/1.3.1/assets/js/index.php (added)
-
tags/1.3.1/assets/js/modules.js (added)
-
tags/1.3.1/blaminhor-essentials.php (added)
-
tags/1.3.1/includes (added)
-
tags/1.3.1/includes/class-blaminhor-essentials-admin.php (added)
-
tags/1.3.1/includes/class-blaminhor-essentials-module.php (added)
-
tags/1.3.1/includes/functions.php (added)
-
tags/1.3.1/includes/index.php (added)
-
tags/1.3.1/index.php (added)
-
tags/1.3.1/languages (added)
-
tags/1.3.1/languages/blaminhor-essentials-de_DE.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-de_DE.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-es_ES.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-es_ES.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-fr_FR.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-fr_FR.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-id_ID.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-id_ID.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-it_IT.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-it_IT.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-ja.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-ja.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-nl_NL.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-nl_NL.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-pt_BR.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-pt_BR.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-pt_PT.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-pt_PT.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-ru_RU.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-ru_RU.po (added)
-
tags/1.3.1/languages/blaminhor-essentials-tr_TR.mo (added)
-
tags/1.3.1/languages/blaminhor-essentials-tr_TR.po (added)
-
tags/1.3.1/languages/blaminhor-essentials.pot (added)
-
tags/1.3.1/languages/index.php (added)
-
tags/1.3.1/modules (added)
-
tags/1.3.1/modules/backup (added)
-
tags/1.3.1/modules/backup/class-module-backup.php (added)
-
tags/1.3.1/modules/backup/index.php (added)
-
tags/1.3.1/modules/broken-links (added)
-
tags/1.3.1/modules/broken-links/class-module-broken-links.php (added)
-
tags/1.3.1/modules/broken-links/index.php (added)
-
tags/1.3.1/modules/classic-editor (added)
-
tags/1.3.1/modules/classic-editor/class-module-classic-editor.php (added)
-
tags/1.3.1/modules/db-optimizer (added)
-
tags/1.3.1/modules/db-optimizer/class-module-db-optimizer.php (added)
-
tags/1.3.1/modules/db-optimizer/index.php (added)
-
tags/1.3.1/modules/domain-changer (added)
-
tags/1.3.1/modules/domain-changer/class-module-domain-changer.php (added)
-
tags/1.3.1/modules/domain-changer/index.php (added)
-
tags/1.3.1/modules/duplicator (added)
-
tags/1.3.1/modules/duplicator/class-module-duplicator.php (added)
-
tags/1.3.1/modules/duplicator/index.php (added)
-
tags/1.3.1/modules/duplicator/views (added)
-
tags/1.3.1/modules/fatal-error-recovery (added)
-
tags/1.3.1/modules/fatal-error-recovery/class-module-fatal-error-recovery.php (added)
-
tags/1.3.1/modules/favicon (added)
-
tags/1.3.1/modules/favicon/class-module-favicon.php (added)
-
tags/1.3.1/modules/favicon/index.php (added)
-
tags/1.3.1/modules/https-redirect (added)
-
tags/1.3.1/modules/https-redirect/class-module-https-redirect.php (added)
-
tags/1.3.1/modules/https-redirect/index.php (added)
-
tags/1.3.1/modules/image-sizes (added)
-
tags/1.3.1/modules/image-sizes/class-module-image-sizes.php (added)
-
tags/1.3.1/modules/image-sizes/index.php (added)
-
tags/1.3.1/modules/index.php (added)
-
tags/1.3.1/modules/maintenance (added)
-
tags/1.3.1/modules/maintenance/class-module-maintenance.php (added)
-
tags/1.3.1/modules/maintenance/index.php (added)
-
tags/1.3.1/modules/mute-core-emails (added)
-
tags/1.3.1/modules/mute-core-emails/class-module-mute-core-emails.php (added)
-
tags/1.3.1/modules/mute-core-emails/index.php (added)
-
tags/1.3.1/modules/redirections (added)
-
tags/1.3.1/modules/redirections/class-module-redirections.php (added)
-
tags/1.3.1/modules/redirections/index.php (added)
-
tags/1.3.1/modules/seo-manager (added)
-
tags/1.3.1/modules/seo-manager/class-module-seo-manager.php (added)
-
tags/1.3.1/modules/seo-manager/index.php (added)
-
tags/1.3.1/modules/smtp (added)
-
tags/1.3.1/modules/smtp/class-module-smtp.php (added)
-
tags/1.3.1/modules/smtp/index.php (added)
-
tags/1.3.1/modules/smtp/views (added)
-
tags/1.3.1/readme.txt (added)
-
tags/1.3.1/uninstall.php (added)
-
trunk/assets/js/modules.js (modified) (6 diffs)
-
trunk/blaminhor-essentials.php (modified) (2 diffs)
-
trunk/includes/class-blaminhor-essentials-admin.php (modified) (2 diffs)
-
trunk/languages/blaminhor-essentials-fr_FR.mo (modified) (previous)
-
trunk/languages/blaminhor-essentials-fr_FR.po (modified) (8 diffs)
-
trunk/modules/backup/class-module-backup.php (modified) (6 diffs)
-
trunk/modules/duplicator/class-module-duplicator.php (modified) (4 diffs)
-
trunk/modules/seo-manager/class-module-seo-manager.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
blaminhor-essentials/trunk/assets/js/modules.js
r3447961 r3448097 946 946 var files = e.originalEvent.dataTransfer.files; 947 947 if (files.length > 0) { 948 self. uploadBackup(files[0]);948 self.addFilesToQueue(files); 949 949 } 950 950 }); … … 958 958 $fileInput.off('change' + ns).on('change' + ns, function() { 959 959 if (this.files.length > 0) { 960 self.uploadBackup(this.files[0]); 961 } 960 self.addFilesToQueue(this.files); 961 this.value = ''; // Reset to allow selecting same files again 962 } 963 }); 964 965 // Start upload button 966 $('#ap-start-upload').off('click' + ns).on('click' + ns, function() { 967 self.processUploadQueue(); 968 }); 969 970 // Clear queue button 971 $('#ap-clear-queue').off('click' + ns).on('click' + ns, function() { 972 self.clearUploadQueue(); 973 }); 974 975 // Remove file from queue (delegated event) 976 $('#ap-file-list').off('click' + ns, '.ap-remove-file').on('click' + ns, '.ap-remove-file', function() { 977 var index = $(this).data('index'); 978 self.removeFileFromQueue(index); 962 979 }); 963 980 … … 1082 1099 1083 1100 backupComplete: function() { 1101 var self = this; 1084 1102 var $btn = $('#ap-create-backup'); 1085 1103 $btn.prop('disabled', false); … … 1088 1106 $('#ap-backup-progress-status').text(this.strings.complete || 'Complete!'); 1089 1107 1090 setTimeout(function() { 1108 // Fetch updated backup list and switch to backups tab 1109 $.post(this.ajaxurl, { 1110 action: 'ap_backup_get_list', 1111 nonce: this.nonce 1112 }, function(response) { 1091 1113 $('#ap-backup-progress').hide(); 1092 $('#ap-backup-result').html( 1093 '<div class="blaminhor-essentials-notice success" style="display: flex; align-items: center; gap: 10px;">' + 1094 '<span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px;"></span>' + 1095 '<span>' + (this.strings.backupSuccess || 'Backup created successfully!') + '</span>' + 1096 '</div>' 1097 ).show(); 1098 setTimeout(function() { location.reload(); }, 1500); 1099 }, 500); 1114 1115 if (response.success) { 1116 // Update the backup count in the tab 1117 self.updateBackupCount(response.data.count); 1118 1119 // Update the backup table content 1120 var $tabContent = $('[data-tab-content="backups"]'); 1121 var $h3 = $tabContent.find('h3').first(); 1122 1123 // Remove existing table/notice and restore modal (we'll keep the modal) 1124 var $restoreModal = $tabContent.find('#ap-restore-modal').detach(); 1125 $tabContent.find('#ap-backups-table, .blaminhor-essentials-notice.warning').remove(); 1126 1127 // Insert new table/notice after h3 1128 $h3.after(response.data.html); 1129 1130 // Re-attach restore modal at the end 1131 if ($restoreModal.length) { 1132 $tabContent.append($restoreModal); 1133 } 1134 1135 // Re-bind sortable table events 1136 self.bindSortable(); 1137 1138 // Switch to the backups tab 1139 if (typeof window.BlaminhorEssentialsActivateTab === 'function') { 1140 window.BlaminhorEssentialsActivateTab('backups'); 1141 } else { 1142 // Fallback: manually switch tabs 1143 $('.blaminhor-essentials-tab').removeClass('active'); 1144 $('[data-tab="backups"]').addClass('active'); 1145 $('.blaminhor-essentials-tab-content').removeClass('active'); 1146 $tabContent.addClass('active'); 1147 } 1148 1149 // Show success message at the top of the backups tab 1150 var successHtml = '<div class="blaminhor-essentials-notice success ap-backup-success-notice" style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">' + 1151 '<span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px;"></span>' + 1152 '<span>' + (self.strings.backupSuccess || 'Backup created successfully!') + '</span>' + 1153 '</div>'; 1154 1155 // Remove any existing success notice and prepend new one 1156 $tabContent.find('.ap-backup-success-notice').remove(); 1157 $h3.after(successHtml); 1158 1159 // Hide the result area on the create tab 1160 $('#ap-backup-result').hide(); 1161 } else { 1162 // Fallback to page reload on error 1163 location.reload(); 1164 } 1165 }).fail(function() { 1166 // Fallback to page reload on network error 1167 location.reload(); 1168 }); 1100 1169 }, 1101 1170 … … 1110 1179 1111 1180 backupState: null, 1181 uploadQueue: [], 1182 1183 addFilesToQueue: function(files) { 1184 var self = this; 1185 var validExtensions = ['.zip']; 1186 1187 for (var i = 0; i < files.length; i++) { 1188 var file = files[i]; 1189 var ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); 1190 1191 if (validExtensions.indexOf(ext) === -1) { 1192 continue; // Skip non-ZIP files 1193 } 1194 1195 // Check if file is already in queue 1196 var exists = false; 1197 for (var j = 0; j < this.uploadQueue.length; j++) { 1198 if (this.uploadQueue[j].name === file.name && this.uploadQueue[j].size === file.size) { 1199 exists = true; 1200 break; 1201 } 1202 } 1203 1204 if (!exists) { 1205 this.uploadQueue.push(file); 1206 } 1207 } 1208 1209 this.renderUploadQueue(); 1210 }, 1211 1212 removeFileFromQueue: function(index) { 1213 this.uploadQueue.splice(index, 1); 1214 this.renderUploadQueue(); 1215 }, 1216 1217 clearUploadQueue: function() { 1218 this.uploadQueue = []; 1219 this.renderUploadQueue(); 1220 }, 1221 1222 renderUploadQueue: function() { 1223 var $queue = $('#ap-upload-queue'); 1224 var $list = $('#ap-file-list'); 1225 var $dropzone = $('#ap-backup-dropzone'); 1226 1227 if (this.uploadQueue.length === 0) { 1228 $queue.hide(); 1229 $dropzone.show(); 1230 return; 1231 } 1232 1233 $dropzone.hide(); 1234 $queue.show(); 1235 1236 var html = ''; 1237 for (var i = 0; i < this.uploadQueue.length; i++) { 1238 var file = this.uploadQueue[i]; 1239 var size = this.formatFileSize(file.size); 1240 html += '<li style="display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #f6f7f7; border-radius: 4px; margin-bottom: 5px;">' + 1241 '<span><span class="dashicons dashicons-media-archive" style="color: #2271b1; margin-right: 8px;"></span>' + file.name + ' <small style="color: #646970;">(' + size + ')</small></span>' + 1242 '<button type="button" class="ap-remove-file" data-index="' + i + '" style="background: none; border: none; cursor: pointer; color: #d63638; padding: 0;"><span class="dashicons dashicons-no-alt"></span></button>' + 1243 '</li>'; 1244 } 1245 $list.html(html); 1246 }, 1247 1248 formatFileSize: function(bytes) { 1249 if (bytes === 0) return '0 B'; 1250 var k = 1024; 1251 var sizes = ['B', 'KB', 'MB', 'GB']; 1252 var i = Math.floor(Math.log(bytes) / Math.log(k)); 1253 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 1254 }, 1255 1256 processUploadQueue: function() { 1257 if (this.uploadQueue.length === 0) { 1258 return; 1259 } 1260 1261 var self = this; 1262 this.uploadQueueIndex = 0; 1263 this.uploadResults = []; 1264 1265 // Hide queue, show progress 1266 $('#ap-upload-queue').hide(); 1267 $('#ap-upload-progress').show(); 1268 $('#ap-upload-result').hide(); 1269 1270 this.uploadNextInQueue(); 1271 }, 1272 1273 uploadNextInQueue: function() { 1274 var self = this; 1275 1276 if (this.uploadQueueIndex >= this.uploadQueue.length) { 1277 // All done - switch to backups tab 1278 this.onUploadQueueComplete(); 1279 return; 1280 } 1281 1282 var file = this.uploadQueue[this.uploadQueueIndex]; 1283 var $progressFill = $('#ap-upload-progress-fill'); 1284 var $status = $('#ap-upload-status'); 1285 var $currentFile = $('#ap-upload-current-file'); 1286 1287 $currentFile.text((this.strings.uploading || 'Uploading') + ' ' + file.name + ' (' + (this.uploadQueueIndex + 1) + '/' + this.uploadQueue.length + ')'); 1288 $progressFill.css('width', '0%'); 1289 1290 var formData = new FormData(); 1291 formData.append('action', 'ap_backup_upload'); 1292 formData.append('nonce', this.nonce); 1293 formData.append('backup_file', file); 1294 1295 $.ajax({ 1296 url: this.ajaxurl, 1297 type: 'POST', 1298 data: formData, 1299 processData: false, 1300 contentType: false, 1301 xhr: function() { 1302 var xhr = new window.XMLHttpRequest(); 1303 xhr.upload.addEventListener('progress', function(e) { 1304 if (e.lengthComputable) { 1305 var percent = (e.loaded / e.total) * 100; 1306 $progressFill.css('width', percent + '%'); 1307 if (percent >= 100) { 1308 $status.text(self.strings.processing || 'Processing...'); 1309 } 1310 } 1311 }, false); 1312 return xhr; 1313 }, 1314 success: function(response) { 1315 self.uploadResults.push({ 1316 file: file.name, 1317 success: response.success, 1318 data: response.data 1319 }); 1320 self.uploadQueueIndex++; 1321 self.uploadNextInQueue(); 1322 }, 1323 error: function() { 1324 self.uploadResults.push({ 1325 file: file.name, 1326 success: false, 1327 data: self.strings.error || 'Upload failed' 1328 }); 1329 self.uploadQueueIndex++; 1330 self.uploadNextInQueue(); 1331 } 1332 }); 1333 }, 1334 1335 onUploadQueueComplete: function() { 1336 var self = this; 1337 1338 // Clear the queue 1339 this.uploadQueue = []; 1340 1341 // Fetch updated backup list and switch to backups tab 1342 $.post(this.ajaxurl, { 1343 action: 'ap_backup_get_list', 1344 nonce: this.nonce 1345 }, function(response) { 1346 $('#ap-upload-progress').hide(); 1347 $('#ap-backup-dropzone').show(); 1348 1349 if (response.success) { 1350 // Update the backup count in the tab 1351 self.updateBackupCount(response.data.count); 1352 1353 // Update the backup table content 1354 var $tabContent = $('[data-tab-content="backups"]'); 1355 var $h3 = $tabContent.find('h3').first(); 1356 1357 // Remove existing table/notice and restore modal 1358 var $restoreModal = $tabContent.find('#ap-restore-modal').detach(); 1359 $tabContent.find('#ap-backups-table, .blaminhor-essentials-notice.warning').remove(); 1360 1361 // Insert new table/notice after h3 1362 $h3.after(response.data.html); 1363 1364 // Re-attach restore modal at the end 1365 if ($restoreModal.length) { 1366 $tabContent.append($restoreModal); 1367 } 1368 1369 // Re-bind sortable table events 1370 self.bindSortable(); 1371 1372 // Switch to the backups tab 1373 if (typeof window.BlaminhorEssentialsActivateTab === 'function') { 1374 window.BlaminhorEssentialsActivateTab('backups'); 1375 } else { 1376 $('.blaminhor-essentials-tab').removeClass('active'); 1377 $('[data-tab="backups"]').addClass('active'); 1378 $('.blaminhor-essentials-tab-content').removeClass('active'); 1379 $tabContent.addClass('active'); 1380 } 1381 1382 // Build success message 1383 var successCount = 0; 1384 var errorCount = 0; 1385 for (var i = 0; i < self.uploadResults.length; i++) { 1386 if (self.uploadResults[i].success) { 1387 successCount++; 1388 } else { 1389 errorCount++; 1390 } 1391 } 1392 1393 var successHtml = ''; 1394 if (successCount > 0) { 1395 var msg = successCount === 1 1396 ? (self.strings.uploadSuccess || 'Backup uploaded successfully!') 1397 : successCount + ' ' + (self.strings.backupsUploaded || 'backups uploaded successfully!'); 1398 successHtml = '<div class="blaminhor-essentials-notice success ap-backup-success-notice" style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">' + 1399 '<span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px;"></span>' + 1400 '<span>' + msg + '</span>' + 1401 '</div>'; 1402 } 1403 1404 if (errorCount > 0) { 1405 successHtml += '<div class="blaminhor-essentials-notice error ap-backup-error-notice" style="margin-bottom: 15px;">' + 1406 '<span class="dashicons dashicons-warning"></span> ' + 1407 errorCount + ' ' + (self.strings.uploadsFailed || 'file(s) failed to upload.') + 1408 '</div>'; 1409 } 1410 1411 // Remove any existing notices and add new ones 1412 $tabContent.find('.ap-backup-success-notice, .ap-backup-error-notice, .ap-backup-domain-notice').remove(); 1413 if (successHtml) { 1414 $h3.after(successHtml); 1415 } 1416 } else { 1417 location.reload(); 1418 } 1419 }).fail(function() { 1420 location.reload(); 1421 }); 1422 }, 1112 1423 1113 1424 downloadBackup: function(filename) { … … 1277 1588 1278 1589 if (response.success) { 1279 $success.show(); 1280 $('#ap-upload-filename').text(response.data.filename + ' (' + response.data.size + ')'); 1281 1282 // Check if domain differs 1283 if (response.data.domain_differs) { 1284 $domainWarning.show(); 1285 $('#ap-upload-domain-info').html( 1286 (self.strings.backupFrom || 'This backup is from') + ' <strong>' + response.data.backup_domain + '</strong>. ' + 1287 (self.strings.currentDomain || 'Current domain is') + ' <strong>' + response.data.current_domain + '</strong>.' 1288 ); 1289 } 1290 1291 // Refresh backups list after short delay 1292 setTimeout(function() { 1590 // Fetch updated backup list and switch to backups tab 1591 $.post(self.ajaxurl, { 1592 action: 'ap_backup_get_list', 1593 nonce: self.nonce 1594 }, function(listResponse) { 1595 if (listResponse.success) { 1596 // Update the backup count in the tab 1597 self.updateBackupCount(listResponse.data.count); 1598 1599 // Update the backup table content 1600 var $tabContent = $('[data-tab-content="backups"]'); 1601 var $h3 = $tabContent.find('h3').first(); 1602 1603 // Remove existing table/notice and restore modal 1604 var $restoreModal = $tabContent.find('#ap-restore-modal').detach(); 1605 $tabContent.find('#ap-backups-table, .blaminhor-essentials-notice.warning').remove(); 1606 1607 // Insert new table/notice after h3 1608 $h3.after(listResponse.data.html); 1609 1610 // Re-attach restore modal at the end 1611 if ($restoreModal.length) { 1612 $tabContent.append($restoreModal); 1613 } 1614 1615 // Re-bind sortable table events 1616 self.bindSortable(); 1617 1618 // Switch to the backups tab 1619 if (typeof window.BlaminhorEssentialsActivateTab === 'function') { 1620 window.BlaminhorEssentialsActivateTab('backups'); 1621 } else { 1622 // Fallback: manually switch tabs 1623 $('.blaminhor-essentials-tab').removeClass('active'); 1624 $('[data-tab="backups"]').addClass('active'); 1625 $('.blaminhor-essentials-tab-content').removeClass('active'); 1626 $tabContent.addClass('active'); 1627 } 1628 1629 // Build success message with domain warning if needed 1630 var successMsg = self.strings.uploadSuccess || 'Backup uploaded successfully!'; 1631 var successHtml = '<div class="blaminhor-essentials-notice success ap-backup-success-notice" style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">' + 1632 '<span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px;"></span>' + 1633 '<span>' + successMsg + ' (' + response.data.filename + ')</span>' + 1634 '</div>'; 1635 1636 // Add domain warning if needed 1637 if (response.data.domain_differs) { 1638 successHtml += '<div class="blaminhor-essentials-notice warning ap-backup-domain-notice" style="margin-bottom: 15px;">' + 1639 '<span class="dashicons dashicons-info" style="color: #dba617;"></span> ' + 1640 (self.strings.backupFrom || 'This backup is from') + ' <strong>' + response.data.backup_domain + '</strong>. ' + 1641 (self.strings.currentDomain || 'Current domain is') + ' <strong>' + response.data.current_domain + '</strong>.' + 1642 '</div>'; 1643 } 1644 1645 // Remove any existing notices and add new ones 1646 $tabContent.find('.ap-backup-success-notice, .ap-backup-domain-notice').remove(); 1647 $h3.after(successHtml); 1648 1649 // Reset upload area 1650 $result.hide(); 1651 $success.hide(); 1652 $domainWarning.hide(); 1653 } else { 1654 // Fallback to page reload 1655 location.reload(); 1656 } 1657 }).fail(function() { 1293 1658 location.reload(); 1294 } , 2000);1659 }); 1295 1660 } else { 1296 1661 $error.show(); -
blaminhor-essentials/trunk/blaminhor-essentials.php
r3447961 r3448097 4 4 * Plugin URI: https://wp.blaminhor.com/ 5 5 * Description: A modular toolkit for WordPress with activatable features. Lightweight, secure, and reliable. 6 * Version: 1.3 6 * Version: 1.3.1 7 7 * Requires at least: 6.2 8 8 * Requires PHP: 7.4 … … 23 23 24 24 // Plugin constants 25 define('BLAMINHOR_ESSENTIALS_VERSION', '1.3 ');25 define('BLAMINHOR_ESSENTIALS_VERSION', '1.3.1'); 26 26 define('BLAMINHOR_ESSENTIALS_PLUGIN_FILE', __FILE__); 27 27 define('BLAMINHOR_ESSENTIALS_PLUGIN_DIR', plugin_dir_path(__FILE__)); -
blaminhor-essentials/trunk/includes/class-blaminhor-essentials-admin.php
r3447753 r3448097 39 39 // Check for activation redirect 40 40 add_action( 'admin_init', array( $this, 'maybe_redirect_after_activation' ) ); 41 42 // Add admin bar menu 43 add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_menu' ), 100 ); 41 44 } 42 45 … … 151 154 foreach ($modules_to_sort as $module_data) { 152 155 $module_data['module']->add_admin_menu(); 156 } 157 } 158 159 /** 160 * Add admin bar menu 161 * 162 * @param WP_Admin_Bar $wp_admin_bar The admin bar instance. 163 */ 164 public function add_admin_bar_menu( $wp_admin_bar ) { 165 // Only show for users who can manage options 166 if ( ! current_user_can( 'manage_options' ) ) { 167 return; 168 } 169 170 // Only show in admin area 171 if ( ! is_admin() ) { 172 return; 173 } 174 175 // Bow tie SVG icon for admin bar (same as sidebar) 176 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode 177 $bow_tie_svg = 'data:image/svg+xml;base64,' . base64_encode( 178 '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">' . 179 '<path fill="#a0a5aa" d="M1 5l6 5-6 5V5zm18 0l-6 5 6 5V5zM8 8h4v4H8z"/>' . 180 '</svg>' 181 ); 182 183 // Add main menu item 184 $wp_admin_bar->add_node( array( 185 'id' => 'blaminhor-essentials', 186 'title' => '<img src="' . esc_attr( $bow_tie_svg ) . '" alt="" style="height: 20px; width: 20px; vertical-align: middle; margin-right: 6px; margin-top: -2px;">' . esc_html__( 'Blaminhor', 'blaminhor-essentials' ), 187 'href' => admin_url( 'admin.php?page=blaminhor-essentials' ), 188 'meta' => array( 189 'title' => __( 'Blaminhor Essentials', 'blaminhor-essentials' ), 190 ), 191 ) ); 192 193 // Add Dashboard submenu 194 $wp_admin_bar->add_node( array( 195 'id' => 'blaminhor-essentials-dashboard', 196 'parent' => 'blaminhor-essentials', 197 'title' => __( 'Dashboard', 'blaminhor-essentials' ), 198 'href' => admin_url( 'admin.php?page=blaminhor-essentials' ), 199 ) ); 200 201 // Get active modules sorted alphabetically by translated name 202 $active_modules = $this->plugin->get_active_modules(); 203 204 if ( empty( $active_modules ) ) { 205 return; 206 } 207 208 // Build array with names for sorting 209 $modules_to_sort = array(); 210 foreach ( $active_modules as $module_id => $module ) { 211 $module_name = method_exists( $module, 'get_name' ) ? $module->get_name() : $module_id; 212 $modules_to_sort[ $module_id ] = array( 213 'module' => $module, 214 'name' => $module_name, 215 ); 216 } 217 218 // Sort by translated name (case-insensitive, accent-insensitive) 219 uasort( $modules_to_sort, function( $a, $b ) { 220 $name_a = remove_accents( $a['name'] ); 221 $name_b = remove_accents( $b['name'] ); 222 return strcasecmp( $name_a, $name_b ); 223 } ); 224 225 // Add sorted module submenus 226 foreach ( $modules_to_sort as $module_id => $module_data ) { 227 $wp_admin_bar->add_node( array( 228 'id' => 'blaminhor-essentials-' . $module_id, 229 'parent' => 'blaminhor-essentials', 230 'title' => $module_data['name'], 231 'href' => admin_url( 'admin.php?page=blaminhor-essentials-' . $module_id ), 232 ) ); 153 233 } 154 234 } -
blaminhor-essentials/trunk/languages/blaminhor-essentials-fr_FR.po
r3447961 r3448097 647 647 648 648 msgid "Enable Coming Soon or Maintenance mode for your site." 649 msgstr "Activer le mode Coming Soon ou Maintenance pour votre site."649 msgstr "Activer le mode En construction ou Maintenance pour votre site." 650 650 651 651 msgid "Coming Soon" 652 msgstr " Bientôt disponible"652 msgstr "En construction" 653 653 654 654 msgid "Under Maintenance" … … 670 670 671 671 msgid "Coming Soon Mode Active" 672 msgstr "Mode Coming Soon actif"672 msgstr "Mode En construction actif" 673 673 674 674 msgid "Maintenance Mode Active" … … 682 682 683 683 msgid "Activate maintenance/coming soon mode" 684 msgstr "Activer le mode maintenance/ coming soon"684 msgstr "Activer le mode maintenance/en construction" 685 685 686 686 msgid "Mode is currently ACTIVE. Visitors cannot access your site." … … 700 700 701 701 msgid "Coming Soon Mode" 702 msgstr "Mode Coming Soon"702 msgstr "Mode En construction" 703 703 704 704 msgid "For new sites not yet ready to launch." … … 718 718 719 719 msgid "Coming Soon / Under Maintenance" 720 msgstr " Bientôt disponible/ En maintenance"720 msgstr "En construction / En maintenance" 721 721 722 722 msgid "Basic HTML allowed." … … 1166 1166 # Favicon Generator Module 1167 1167 msgid "Favicon Generator" 1168 msgstr " Générateur deFavicon"1168 msgstr "Favicon" 1169 1169 1170 1170 msgid "Generate all favicon formats from a single image." … … 1509 1509 # Maintenance mode renamed 1510 1510 msgid "Coming soon / Maintenance" 1511 msgstr " Coming soon / Maintenance"1511 msgstr "En construction / Maintenance" 1512 1512 1513 1513 # AJAX confirm … … 2235 2235 msgid "Backup created successfully!" 2236 2236 msgstr "Sauvegarde créée avec succès !" 2237 2238 msgid "Backup uploaded successfully!" 2239 msgstr "Sauvegarde téléversée avec succès !" 2240 2241 msgid "Uploading" 2242 msgstr "Téléversement de" 2243 2244 msgid "backups uploaded successfully!" 2245 msgstr "sauvegardes téléversées avec succès !" 2246 2247 msgid "file(s) failed to upload." 2248 msgstr "fichier(s) n'ont pas pu être téléversés." 2249 2250 msgid "Selected Files" 2251 msgstr "Fichiers sélectionnés" 2252 2253 msgid "Upload All Files" 2254 msgstr "Téléverser tous les fichiers" 2255 2256 msgid "Clear" 2257 msgstr "Effacer" 2258 2259 msgid "Drag and drop backup files here" 2260 msgstr "Glissez-déposez des fichiers de sauvegarde ici" 2261 2262 msgid "Maximum upload size: %s per file" 2263 msgstr "Taille maximale de téléversement : %s par fichier" 2237 2264 2238 2265 msgid "Backup deleted successfully." -
blaminhor-essentials/trunk/modules/backup/class-module-backup.php
r3447961 r3448097 93 93 add_action( 'wp_ajax_ap_backup_get_domain_info', array( $this, 'ajax_get_domain_info' ) ); 94 94 add_action( 'wp_ajax_ap_backup_create_step', array( $this, 'ajax_create_backup_step' ) ); 95 add_action( 'wp_ajax_ap_backup_get_list', array( $this, 'ajax_get_backup_list' ) ); 95 96 96 97 // Scheduled backup cron. … … 133 134 'creating' => __( 'Backing up', 'blaminhor-essentials' ), 134 135 'complete' => __( 'Complete!', 'blaminhor-essentials' ), 135 'backupSuccess' => __( 'Backup created successfully!', 'blaminhor-essentials' ), 136 'backupSuccess' => __( 'Backup created successfully!', 'blaminhor-essentials' ), 137 'uploadSuccess' => __( 'Backup uploaded successfully!', 'blaminhor-essentials' ), 138 'uploading' => __( 'Uploading', 'blaminhor-essentials' ), 139 'backupsUploaded' => __( 'backups uploaded successfully!', 'blaminhor-essentials' ), 140 'uploadsFailed' => __( 'file(s) failed to upload.', 'blaminhor-essentials' ), 136 141 ), 137 142 ); … … 1901 1906 ucfirst( $component ) 1902 1907 ), 1908 ) ); 1909 } 1910 1911 /** 1912 * AJAX: Get backup list HTML and count 1913 */ 1914 public function ajax_get_backup_list() { 1915 check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' ); 1916 1917 if ( ! current_user_can( 'manage_options' ) ) { 1918 wp_send_json_error( __( 'Unauthorized', 'blaminhor-essentials' ) ); 1919 } 1920 1921 $backups = $this->get_backups(); 1922 $count = count( $backups ); 1923 1924 // Generate HTML for the backup table. 1925 ob_start(); 1926 if ( empty( $backups ) ) : 1927 ?> 1928 <div class="blaminhor-essentials-notice warning"> 1929 <?php esc_html_e( 'No backups found. Create your first backup now!', 'blaminhor-essentials' ); ?> 1930 </div> 1931 <?php 1932 else : 1933 ?> 1934 <table class="widefat striped ap-sortable-table" id="ap-backups-table"> 1935 <thead> 1936 <tr> 1937 <th class="ap-sortable" data-sort="string"><?php esc_html_e( 'Backup', 'blaminhor-essentials' ); ?> <span class="ap-sort-icon"></span></th> 1938 <th class="ap-sortable" data-sort="string" style="width: 100px;"><?php esc_html_e( 'Type', 'blaminhor-essentials' ); ?> <span class="ap-sort-icon"></span></th> 1939 <th class="ap-sortable" data-sort="number" style="width: 100px;"><?php esc_html_e( 'Size', 'blaminhor-essentials' ); ?> <span class="ap-sort-icon"></span></th> 1940 <th class="ap-sortable desc" data-sort="number" style="width: 160px;"><?php esc_html_e( 'Date', 'blaminhor-essentials' ); ?> <span class="ap-sort-icon"></span></th> 1941 <th style="width: 180px;"><?php esc_html_e( 'Actions', 'blaminhor-essentials' ); ?></th> 1942 </tr> 1943 </thead> 1944 <tbody> 1945 <?php foreach ( $backups as $backup ) : ?> 1946 <tr data-prefix="<?php echo esc_attr( $backup['prefix'] ); ?>" data-type="<?php echo esc_attr( $backup['type'] ); ?>" data-size="<?php echo esc_attr( $backup['total_size'] ); ?>" data-date="<?php echo esc_attr( $backup['date'] ); ?>"> 1947 <td> 1948 <code style="font-size: 12px;"><?php echo esc_html( $backup['prefix'] ); ?></code> 1949 <div style="font-size: 11px; color: #646970; margin-top: 3px;"> 1950 <?php 1951 $component_labels = array( 1952 'database' => __( 'DB', 'blaminhor-essentials' ), 1953 'plugins' => __( 'Plugins', 'blaminhor-essentials' ), 1954 'themes' => __( 'Themes', 'blaminhor-essentials' ), 1955 'uploads' => __( 'Uploads', 'blaminhor-essentials' ), 1956 'wp-content' => __( 'WP-Content', 'blaminhor-essentials' ), 1957 'wp-core' => __( 'WP Core', 'blaminhor-essentials' ), 1958 ); 1959 $labels = array(); 1960 foreach ( $backup['components'] as $comp ) { 1961 if ( isset( $component_labels[ $comp ] ) ) { 1962 $labels[] = $component_labels[ $comp ]; 1963 } 1964 } 1965 echo esc_html( implode( ', ', $labels ) ); 1966 ?> 1967 (<?php echo esc_html( count( $backup['archives'] ) ); ?> <?php echo esc_html( _n( 'archive', 'archives', count( $backup['archives'] ), 'blaminhor-essentials' ) ); ?>) 1968 </div> 1969 </td> 1970 <td><span class="ap-backup-type ap-backup-type-<?php echo esc_attr( $backup['type'] ); ?>"><?php echo esc_html( $backup['type_label'] ); ?></span></td> 1971 <td><?php echo esc_html( $backup['size_human'] ); ?></td> 1972 <td><?php echo esc_html( $backup['date_human'] ); ?></td> 1973 <td style="white-space: nowrap;"> 1974 <button type="button" class="button button-small ap-backup-restore" data-prefix="<?php echo esc_attr( $backup['prefix'] ); ?>" data-type="<?php echo esc_attr( $backup['type'] ); ?>" data-components="<?php echo esc_attr( wp_json_encode( $backup['components'] ) ); ?>"> 1975 <?php esc_html_e( 'Restore', 'blaminhor-essentials' ); ?> 1976 </button> 1977 <button type="button" class="button button-small ap-backup-delete" data-prefix="<?php echo esc_attr( $backup['prefix'] ); ?>" style="color: #d63638;"> 1978 <?php esc_html_e( 'Delete', 'blaminhor-essentials' ); ?> 1979 </button> 1980 </td> 1981 </tr> 1982 <?php endforeach; ?> 1983 </tbody> 1984 </table> 1985 <?php 1986 endif; 1987 $html = ob_get_clean(); 1988 1989 // Clear the backup in progress lock. 1990 delete_transient( 'blaminhor_backup_in_progress' ); 1991 1992 wp_send_json_success( array( 1993 'count' => $count, 1994 'html' => $html, 1903 1995 ) ); 1904 1996 } … … 2466 2558 <div class="ap-upload-dropzone" id="ap-backup-dropzone"> 2467 2559 <span class="dashicons dashicons-upload"></span> 2468 <p><?php esc_html_e( 'Drag and drop a backup filehere', 'blaminhor-essentials' ); ?></p>2560 <p><?php esc_html_e( 'Drag and drop backup files here', 'blaminhor-essentials' ); ?></p> 2469 2561 <p class="ap-upload-or"><?php esc_html_e( 'or', 'blaminhor-essentials' ); ?></p> 2470 2562 <label class="button button-secondary"> 2471 2563 <?php esc_html_e( 'Browse Files', 'blaminhor-essentials' ); ?> 2472 <input type="file" id="ap-backup-file-input" accept=".zip" style="display: none;">2564 <input type="file" id="ap-backup-file-input" accept=".zip" multiple style="display: none;"> 2473 2565 </label> 2474 2566 <p class="description" style="margin-top: 15px;"> … … 2477 2569 printf( 2478 2570 /* translators: %s: maximum upload size */ 2479 esc_html__( 'Maximum upload size: %s ', 'blaminhor-essentials' ),2571 esc_html__( 'Maximum upload size: %s per file', 'blaminhor-essentials' ), 2480 2572 esc_html( $max_upload ) 2481 2573 ); … … 2484 2576 </div> 2485 2577 2578 <!-- File Queue --> 2579 <div class="ap-upload-queue" id="ap-upload-queue" style="display: none;"> 2580 <h4 style="margin: 0 0 10px;"><?php esc_html_e( 'Selected Files', 'blaminhor-essentials' ); ?></h4> 2581 <ul class="ap-file-list" id="ap-file-list"></ul> 2582 <div style="margin-top: 15px; display: flex; gap: 10px;"> 2583 <button type="button" class="button button-primary" id="ap-start-upload"> 2584 <span class="dashicons dashicons-upload" style="margin-right: 5px;"></span> 2585 <?php esc_html_e( 'Upload All Files', 'blaminhor-essentials' ); ?> 2586 </button> 2587 <button type="button" class="button" id="ap-clear-queue"> 2588 <?php esc_html_e( 'Clear', 'blaminhor-essentials' ); ?> 2589 </button> 2590 </div> 2591 </div> 2592 2486 2593 <div class="ap-upload-progress" id="ap-upload-progress" style="display: none;"> 2594 <p id="ap-upload-current-file" style="margin-bottom: 10px; font-weight: 500;"></p> 2487 2595 <div class="ap-upload-progress-bar"> 2488 2596 <div class="ap-upload-progress-fill" id="ap-upload-progress-fill"></div> -
blaminhor-essentials/trunk/modules/duplicator/class-module-duplicator.php
r3447961 r3448097 50 50 $current_suffix = $this->get_setting( 'title_suffix', '' ); 51 51 52 // Check for old format (with parentheses) 53 if ( ' (Copy)' === $current_suffix || preg_match( '/^\s*\(.+\)\s*$/', $current_suffix ) ) { 52 // Check for old format (with parentheses in any language) or malformed new format 53 // Old formats: " (Copy)", " (Copie)", "(Kopie)", etc. 54 // Malformed: "- copy", "- Copie" (missing leading space) 55 $needs_migration = false; 56 57 // Check for parentheses format 58 if ( preg_match( '/\(.*\)/', $current_suffix ) ) { 59 $needs_migration = true; 60 } 61 62 // Check for malformed new format (missing leading space before dash) 63 if ( preg_match( '/^-\s/', $current_suffix ) ) { 64 $needs_migration = true; 65 } 66 67 if ( $needs_migration ) { 54 68 $this->settings['title_suffix'] = __( ' - copy', 'blaminhor-essentials' ); 55 69 $this->save_settings( $this->settings ); … … 63 77 */ 64 78 protected function get_default_settings() { 79 // Get all available post types (includes custom post types). 80 $all_post_types = blaminhor_essentials_get_post_types(); 81 $default_post_types = ! empty( $all_post_types ) ? array_keys( $all_post_types ) : array( 'post', 'page' ); 82 83 // Get all available taxonomies (includes custom taxonomies). 84 $all_taxonomies = blaminhor_essentials_get_taxonomies(); 85 $default_taxonomies = ! empty( $all_taxonomies ) ? array_keys( $all_taxonomies ) : array( 'category', 'post_tag' ); 86 65 87 return array( 66 'post_types' => array( 'post', 'page' ),67 'taxonomies' => array( 'category', 'post_tag' ),88 'post_types' => $default_post_types, 89 'taxonomies' => $default_taxonomies, 68 90 'copy_title' => true, 69 91 'title_prefix' => '', … … 360 382 * Copy post meta 361 383 * 384 * Uses direct database query to preserve exact data encoding (important for page builders like Elementor). 385 * 362 386 * @param int $source_id Source post ID. 363 387 * @param int $target_id Target post ID. 364 388 */ 365 389 private function copy_post_meta( $source_id, $target_id ) { 366 $meta = get_post_meta( $source_id );390 global $wpdb; 367 391 368 392 // Meta keys to exclude (internal WordPress + generated CSS that contains post IDs) … … 380 404 ) ); 381 405 382 foreach ( $meta as $key => $values ) { 383 // Skip excluded keys 384 if ( in_array( $key, $excluded, true ) ) { 385 continue; 386 } 387 388 foreach ( $values as $value ) { 389 add_post_meta( $target_id, $key, maybe_unserialize( $value ) ); 390 } 406 // Build exclusion placeholders 407 $excluded_placeholders = implode( ',', array_fill( 0, count( $excluded ), '%s' ) ); 408 409 // Get raw meta data directly from database to preserve exact encoding 410 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 411 $source_meta = $wpdb->get_results( 412 $wpdb->prepare( 413 "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key NOT IN ($excluded_placeholders)", 414 array_merge( array( $source_id ), $excluded ) 415 ) 416 ); 417 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 418 419 // Copy each meta entry directly to preserve encoding 420 foreach ( $source_meta as $meta ) { 421 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 422 $wpdb->insert( 423 $wpdb->postmeta, 424 array( 425 'post_id' => $target_id, 426 'meta_key' => $meta->meta_key, 427 'meta_value' => $meta->meta_value, 428 ), 429 array( '%d', '%s', '%s' ) 430 ); 391 431 } 392 432 -
blaminhor-essentials/trunk/modules/seo-manager/class-module-seo-manager.php
r3447961 r3448097 106 106 // Sitemap functionality. 107 107 if ( $this->get_setting( 'sitemap_enabled', true ) ) { 108 add_action( 'init', array( $this, 'add_sitemap_rewrite_rules' ) ); 109 add_action( 'init', array( $this, 'maybe_flush_rewrite_rules' ), 99 ); 108 // Call directly since we're already in init (plugin loads on init priority 0). 109 $this->add_sitemap_rewrite_rules(); 110 add_action( 'wp_loaded', array( $this, 'maybe_flush_rewrite_rules' ) ); 110 111 add_action( 'template_redirect', array( $this, 'render_sitemap' ), 1 ); // Priority 1 = run early. 111 112 add_filter( 'query_vars', array( $this, 'add_sitemap_query_vars' ) ); … … 899 900 'post_types' => $this->get_setting( 'sitemap_post_types', array( 'post', 'page' ) ), 900 901 'taxonomies' => $this->get_setting( 'sitemap_taxonomies', array( 'category', 'post_tag' ) ), 901 'flush_v 2' => true, // Force flush on update.902 'flush_v3' => true, // Force flush on update (v3: subdirectory fix). 902 903 ) ) ); 903 904 … … 975 976 if ( empty( $path ) ) { 976 977 return false; 978 } 979 980 // Handle subdirectory installations: remove home path prefix. 981 $home_path = wp_parse_url( home_url(), PHP_URL_PATH ); 982 if ( ! empty( $home_path ) ) { 983 $home_path = trailingslashit( $home_path ); 984 if ( strpos( $path, $home_path ) === 0 ) { 985 $path = substr( $path, strlen( $home_path ) ); 986 } 977 987 } 978 988 -
blaminhor-essentials/trunk/readme.txt
r3447961 r3448097 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1.3 7 Stable tag: 1.3.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 194 194 195 195 == Changelog == 196 197 = 1.3.1 = 198 * Fixed: Content Duplicator - Unicode characters (accents, special characters) are now preserved correctly when duplicating page builder content. 199 * Fixed: Content Duplicator - Title suffix now displays correctly with proper spacing (" - Copie" instead of "- copie"). 200 * Fixed: SEO/GSO - XML sitemaps now work correctly on WordPress installations in subdirectories. 201 * Fixed: SEO/GSO - Sitemap rewrite rules timing issue resolved for better compatibility. 202 * Improved: Content Duplicator - Meta data is now copied directly via database to preserve exact encoding for page builders. 203 * Improved: Content Duplicator - All post types and taxonomies (including custom ones) are now enabled by default. 204 * Improved: Backup module - After backup completion, automatically switches to Backups tab with updated list and success message. 205 * Improved: Backup module - Upload tab now supports multiple files with queue management before uploading. 206 * Added: Admin bar menu with quick access to Dashboard and all active modules. 196 207 197 208 = 1.3 = … … 271 282 == Upgrade Notice == 272 283 284 = 1.3.1 = 285 Fixed Unicode character corruption when duplicating page builder content. Fixed XML sitemaps on subdirectory installations. Custom post types and taxonomies now enabled by default. Backup shows updated list after completion. Admin bar menu added for quick access. 286 273 287 = 1.3 = 274 288 9 new languages added. Full page builder support in Content Duplicator (Elementor, Beaver Builder, Divi, etc.). Fixed SMTP import and email log refresh. Added missing HTTPS translations. Various bug fixes and improvements.
Note: See TracChangeset
for help on using the changeset viewer.