Changeset 3450271
- Timestamp:
- 01/30/2026 09:25:24 AM (8 weeks ago)
- Location:
- nhrrob-options-table-manager
- Files:
-
- 10 added
- 26 edited
- 1 copied
-
tags/1.3.0 (copied) (copied from nhrrob-options-table-manager/trunk)
-
tags/1.3.0/assets/css/admin.css (modified) (6 diffs)
-
tags/1.3.0/assets/js/admin.js (modified) (4 diffs)
-
tags/1.3.0/includes/Ajax/AjaxHandler.php (modified) (7 diffs)
-
tags/1.3.0/includes/Cli (added)
-
tags/1.3.0/includes/Cli/CliCommands.php (added)
-
tags/1.3.0/includes/Managers/BetterPaymentTableManager.php (modified) (1 diff)
-
tags/1.3.0/includes/Managers/CommonTableManager.php (modified) (1 diff)
-
tags/1.3.0/includes/Managers/HistoryManager.php (modified) (4 diffs)
-
tags/1.3.0/includes/Managers/ImportExportManager.php (added)
-
tags/1.3.0/includes/Managers/OptimizationManager.php (modified) (2 diffs)
-
tags/1.3.0/includes/Managers/OptionsTableManager.php (modified) (3 diffs)
-
tags/1.3.0/includes/Managers/ScannerManager.php (added)
-
tags/1.3.0/includes/Managers/SearchReplaceManager.php (added)
-
tags/1.3.0/includes/Managers/UsermetaTableManager.php (modified) (1 diff)
-
tags/1.3.0/includes/Managers/WprmRatingsTableManager.php (modified) (1 diff)
-
tags/1.3.0/includes/views/admin/settings/index.php (modified) (9 diffs)
-
tags/1.3.0/nhrrob-options-table-manager.php (modified) (7 diffs)
-
tags/1.3.0/readme.txt (modified) (4 diffs)
-
trunk/assets/css/admin.css (modified) (6 diffs)
-
trunk/assets/js/admin.js (modified) (4 diffs)
-
trunk/includes/Ajax/AjaxHandler.php (modified) (7 diffs)
-
trunk/includes/Cli (added)
-
trunk/includes/Cli/CliCommands.php (added)
-
trunk/includes/Managers/BetterPaymentTableManager.php (modified) (1 diff)
-
trunk/includes/Managers/CommonTableManager.php (modified) (1 diff)
-
trunk/includes/Managers/HistoryManager.php (modified) (4 diffs)
-
trunk/includes/Managers/ImportExportManager.php (added)
-
trunk/includes/Managers/OptimizationManager.php (modified) (2 diffs)
-
trunk/includes/Managers/OptionsTableManager.php (modified) (3 diffs)
-
trunk/includes/Managers/ScannerManager.php (added)
-
trunk/includes/Managers/SearchReplaceManager.php (added)
-
trunk/includes/Managers/UsermetaTableManager.php (modified) (1 diff)
-
trunk/includes/Managers/WprmRatingsTableManager.php (modified) (1 diff)
-
trunk/includes/views/admin/settings/index.php (modified) (9 diffs)
-
trunk/nhrrob-options-table-manager.php (modified) (7 diffs)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
nhrrob-options-table-manager/tags/1.3.0/assets/css/admin.css
r3442179 r3450271 9 9 } 10 10 11 .d-block { 12 display: block; 13 } 14 11 15 .m-auto { 12 16 margin: auto; 13 17 } 14 18 19 .w-full { 20 width: 100%; 21 } 22 23 .flex-1 { 24 flex: 1; 25 } 26 27 .cursor-pointer, 28 .pointer { 29 cursor: pointer; 30 } 31 32 .max-w-600 { 33 max-width: 600px; 34 } 35 36 .d-flex { 37 display: flex; 38 } 39 40 .items-center { 41 align-items: center; 42 } 43 44 .gap-10 { 45 gap: 10px; 46 } 47 48 .gap-20 { 49 gap: 20px; 50 } 51 52 .gap-40 { 53 gap: 40px; 54 } 55 56 .font-semibold { 57 font-weight: 600; 58 } 59 60 .m-0 { 61 margin: 0; 62 } 63 15 64 .m-1 { 16 65 margin: 5px; … … 53 102 } 54 103 104 .mt-30 { 105 margin-top: 30px; 106 } 107 55 108 .mb-1 { 56 109 margin-bottom: 5px; … … 215 268 .text-center { 216 269 text-align: center; 270 } 271 272 .loader-container { 273 text-align: center; 274 padding: 40px; 275 } 276 277 .nhrotm-loader-box { 278 text-align: center; 279 padding: 20px; 280 } 281 282 .nhrotm-tab-card { 283 max-width: 100%; 284 margin-top: 20px; 285 padding: 20px; 286 } 287 288 .nhrotm-card-full { 289 max-width: 100%; 290 margin-top: 20px; 291 padding: 20px; 292 } 293 294 .nhrotm-section-divider { 295 border-top: 1px solid #ddd; 296 padding-top: 20px; 297 margin-top: 30px; 298 } 299 300 .nhrotm-sr-summary { 301 background: #f0f0f1; 302 padding: 15px; 303 border-radius: 4px; 304 margin-bottom: 20px; 217 305 } 218 306 … … 231 319 } 232 320 321 .nhrotm-scrollable-vh-50 { 322 max-height: 500px; 323 overflow-y: auto; 324 } 325 326 .nhrotm-scrollable-vh-30 { 327 max-height: 300px; 328 overflow-y: auto; 329 } 330 233 331 /* Modal design */ 234 332 .nhrotm-add-option-modal, … … 237 335 .nhrotm-history-modal { 238 336 display: none; 239 position: fixed; /* Stay in place */ 240 z-index: 1000; /* Sit on top */ 337 position: fixed; 338 /* Stay in place */ 339 z-index: 1000; 340 /* Sit on top */ 241 341 left: 0; 242 342 top: 0; 243 width: 100%; /* Full width */ 244 height: 100%; /* Full height */ 245 overflow: auto; /* Enable scroll if needed */ 246 background-color: rgba(0, 0, 0, 0.5); /* Black w/ opacity */ 343 width: 100%; 344 /* Full width */ 345 height: 100%; 346 /* Full height */ 347 overflow: auto; 348 /* Enable scroll if needed */ 349 background-color: rgba(0, 0, 0, 0.5); 350 /* Black w/ opacity */ 247 351 } 248 352 249 353 .nhrotm-modal-content { 250 354 background-color: #fefefe; 251 margin: 15% auto; /* 15% from the top and centered */ 355 margin: 15% auto; 356 /* 15% from the top and centered */ 252 357 padding: 20px; 253 358 border: 1px solid #888; 254 width: 80%; /* Could be more or less, depending on screen size */ 255 max-width: 500px; /* Set a max width for larger screens */ 256 border-radius: 8px; /* Rounded corners */ 359 width: 80%; 360 /* Could be more or less, depending on screen size */ 361 max-width: 500px; 362 /* Set a max width for larger screens */ 363 border-radius: 8px; 364 /* Rounded corners */ 365 } 366 367 .nhrotm-modal-lg { 368 max-width: 800px; 369 width: 90%; 257 370 } 258 371 … … 524 637 border-left: 4px solid #2271b1; 525 638 } 639 640 /* Orphan Scanner Styles */ 641 .nhrotm-risk-low { 642 background-color: #d4edda; 643 color: #155724; 644 padding: 2px 8px; 645 border-radius: 12px; 646 font-size: 11px; 647 font-weight: 600; 648 text-transform: uppercase; 649 } 650 651 .nhrotm-risk-medium { 652 background-color: #fff3cd; 653 color: #856404; 654 padding: 2px 8px; 655 border-radius: 12px; 656 font-size: 11px; 657 font-weight: 600; 658 text-transform: uppercase; 659 } 660 661 .nhrotm-risk-high { 662 background-color: #f8d7da; 663 color: #721c24; 664 padding: 2px 8px; 665 border-radius: 12px; 666 font-size: 11px; 667 font-weight: 600; 668 text-transform: uppercase; 669 } 670 671 .nhrotm-scanner-actions { 672 display: flex; 673 justify-content: center; 674 border-top: 1px solid #eee; 675 padding-top: 20px; 676 } 677 678 #nhrotm-scanner-results .wp-list-table { 679 margin-top: 20px; 680 } 681 682 #nhrotm-scanner-results td { 683 vertical-align: middle; 684 } 685 686 .nhrotm-spinner-centered { 687 float: none; 688 margin-bottom: 10px; 689 } 690 691 /* Search & Replace Styles */ 692 .nhrotm-search-replace-form .nhrotm-form-field input[type="text"] { 693 width: 100%; 694 max-width: 400px; 695 } 696 697 .nhrotm-summary-box { 698 border-left: 4px solid #2271b1; 699 } 700 701 .nhrotm-sr-list-body td { 702 vertical-align: middle; 703 } 704 705 /* Import / Export Styles */ 706 .nhrotm-ie-columns { 707 display: flex; 708 flex-wrap: wrap; 709 gap: 40px; 710 } 711 712 .nhrotm-ie-column { 713 min-width: 300px; 714 } 715 716 .nhrotm-ie-border-left { 717 border-left: 1px solid #ddd; 718 padding-left: 40px; 719 } 720 721 /* Export Suggestions */ 722 .nhrotm-suggestions-box { 723 border: 1px solid #ccd0d4; 724 max-height: 200px; 725 overflow-y: auto; 726 background: #fff; 727 position: absolute; 728 width: 95%; 729 /* Approximate relative width */ 730 z-index: 100; 731 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 732 } 733 734 .nhrotm-suggestion-item { 735 padding: 8px 12px; 736 cursor: pointer; 737 border-bottom: 1px solid #f0f0f1; 738 } 739 740 .nhrotm-suggestion-item:hover { 741 background: #f0f0f1; 742 color: #2271b1; 743 } 744 745 /* Export Basket */ 746 .nhrotm-export-basket { 747 background: #f6f7f7; 748 border: 1px solid #dcdcde; 749 padding: 15px; 750 border-radius: 4px; 751 min-height: 100px; 752 max-height: 300px; 753 overflow-y: auto; 754 } 755 756 .nhrotm-export-basket ul { 757 margin: 0; 758 padding: 0; 759 list-style: none; 760 } 761 762 .nhrotm-export-basket li { 763 background: #fff; 764 border: 1px solid #c3c4c7; 765 margin-bottom: 5px; 766 padding: 6px 10px; 767 border-radius: 3px; 768 display: flex; 769 justify-content: space-between; 770 align-items: center; 771 } 772 773 .nhrotm-basket-remove { 774 cursor: pointer; 775 color: #d63638; 776 } 777 778 .nhrotm-basket-remove:hover { 779 color: #b32d2e; 780 } 781 782 .empty-basket { 783 color: #646970; 784 font-style: italic; 785 background: transparent !important; 786 border: none !important; 787 padding: 0 !important; 788 } 789 790 /* Import Preview */ 791 .nhrotm-status-badge { 792 padding: 2px 8px; 793 border-radius: 12px; 794 font-size: 11px; 795 font-weight: 600; 796 text-transform: uppercase; 797 } 798 799 .nhrotm-status-badge.new { 800 background-color: #d4edda; 801 color: #155724; 802 } 803 804 .nhrotm-status-badge.modified, 805 .nhrotm-status-badge.changed { 806 background-color: #fff3cd; 807 color: #856404; 808 } 809 810 .nhrotm-status-badge.unchanged, 811 .nhrotm-status-badge.none { 812 background-color: #e2e4e7; 813 color: #646970; 814 } 815 816 .nhrotm-scrollable-table { 817 border: 1px solid #c3c4c7; 818 } 819 820 .nhrotm-scrollable-table .wp-list-table { 821 margin: 0; 822 border: none; 823 box-shadow: none; 824 } -
nhrrob-options-table-manager/tags/1.3.0/assets/js/admin.js
r3442179 r3450271 849 849 850 850 // Handle Global UI Elements visibility 851 // Feature tabs don't show filters or "Add Option" 852 const isFeatureTab = $(this).hasClass('optimization-tab') || $(this).hasClass('settings-tab'); 853 851 852 // "Add New Option" button - Visible ONLY for "Options Table" 853 if ($(this).hasClass('options-table')) { 854 $('.nhrotm-add-option-button').show(); 855 } else { 856 $('.nhrotm-add-option-button').hide(); 857 } 858 859 // Filters and User ID - Hidden for Feature tabs 860 const isFeatureTab = $(this).hasClass('optimization-tab') || $(this).hasClass('settings-tab') || $(this).hasClass('scanner-tab') || $(this).hasClass('search-replace-tab') || $(this).hasClass('import-export-tab'); 861 854 862 if (isFeatureTab) { 855 863 $('.nhrotm-filter-container').hide(); 856 $('.nhrotm-add-option-button').hide();857 864 $('.logged-user-id').hide(); 858 865 } else { … … 860 867 $('.logged-user-id').show(); 861 868 862 // "Add Option" is specifically for the main Options Table863 if ($(this).hasClass('options-table')) {864 $('.nhrotm-add-option-button').show();865 } else {866 $('.nhrotm-add-option-button').hide();867 }868 869 869 // Adjust DataTables inside this tab automatically 870 870 $targetContainer.find('table.nhrotm-data-table').each(function () { … … 878 878 if ($(this).hasClass('optimization-tab')) { 879 879 loadAutoloadData(); 880 } else if ($(this).hasClass('scanner-tab')) { 881 // Potential initial load or reset view 882 } else if ($(this).hasClass('search-replace-tab')) { 883 // Potential reset view 884 $('#nhrotm-search-replace-results').hide(); 885 $('.nhrotm-search-replace-form').show(); 880 886 } 881 887 }); … … 1014 1020 }); 1015 1021 1022 // --- Orphan Scanner Feature --- 1023 1024 $('#nhrotm-start-scan').on('click', function () { 1025 $('.nhrotm-scanner-actions').addClass('d-none'); 1026 $('.nhrotm-scanner-loading').removeClass('d-none'); 1027 $('#nhrotm-scanner-results').addClass('d-none'); 1028 $('#nhrotm-scanner-empty').addClass('d-none'); 1029 1030 $.ajax({ 1031 url: nhrotmOptionsTableManager.ajaxUrl, 1032 method: "GET", 1033 data: { 1034 action: "nhrotm_scan_orphans", 1035 nonce: nhrotmOptionsTableManager.nonce 1036 }, 1037 success: function (response) { 1038 $('.nhrotm-scanner-loading').addClass('d-none'); 1039 $('.nhrotm-scanner-actions').removeClass('d-none'); 1040 1041 if (response.success) { 1042 const orphans = response.data; 1043 if (orphans.length === 0) { 1044 $('#nhrotm-scanner-empty').removeClass('d-none'); 1045 } else { 1046 let html = ''; 1047 orphans.forEach(item => { 1048 html += `<tr> 1049 <td><strong>${item.prefix}</strong></td> 1050 <td>${item.count}</td> 1051 <td>${item.possible_source}</td> 1052 <td><span class="nhrotm-risk-${item.risk.toLowerCase()}">${item.risk}</span></td> 1053 <td> 1054 <button class="button button-danger nhrotm-delete-orphans" data-prefix="${item.prefix}">Delete All</button> 1055 </td> 1056 </tr>`; 1057 }); 1058 $('#nhrotm-scanner-list-body').html(html); 1059 $('#nhrotm-scanner-results').removeClass('d-none'); 1060 } 1061 } else { 1062 showToast("Scan failed: " + response.data, "error"); 1063 } 1064 } 1065 }); 1066 }); 1067 1068 $(document).on('click', '.nhrotm-delete-orphans', function () { 1069 const prefix = $(this).data('prefix'); 1070 1071 if (confirm(`Are you sure you want to delete all options starting with "${prefix}"? This action cannot be undone.`)) { 1072 const $btn = $(this); 1073 $btn.prop('disabled', true).text('Deleting...'); 1074 1075 $.ajax({ 1076 url: nhrotmOptionsTableManager.ajaxUrl, 1077 method: "POST", 1078 data: { 1079 action: "nhrotm_delete_orphaned_prefix", 1080 nonce: nhrotmOptionsTableManager.nonce, 1081 prefix: prefix 1082 }, 1083 success: function (response) { 1084 if (response.success) { 1085 showToast(response.data.message, "success"); 1086 // Refresh scan 1087 $('#nhrotm-start-scan').trigger('click'); 1088 // Also reload main table if it's there 1089 if ($.fn.DataTable.isDataTable('#nhrotm-data-table')) { 1090 $('#nhrotm-data-table').DataTable().ajax.reload(null, false); 1091 } 1092 } else { 1093 showToast("Delete failed: " + response.data, "error"); 1094 $btn.prop('disabled', false).text('Delete All'); 1095 } 1096 } 1097 }); 1098 } 1099 }); 1100 1101 // --- Search & Replace Feature --- 1102 1103 $('#nhrotm-search-replace-btn').on('click', function (e) { 1104 e.preventDefault(); 1105 const search = $('#nhrotm-search-string').val(); 1106 const replace = $('#nhrotm-replace-string').val(); 1107 const dryRun = $('#nhrotm-dry-run-toggle').is(':checked'); 1108 1109 if (!search) { 1110 showToast("Search string is required", "error"); 1111 return; 1112 } 1113 1114 if (!dryRun && !confirm("WARNING: This will permanently modify your database records. Are you sure you want to proceed?")) { 1115 return; 1116 } 1117 1118 $('.nhrotm-search-replace-form').addClass('d-none'); 1119 $('.nhrotm-search-replace-loading').removeClass('d-none'); 1120 $('#nhrotm-search-replace-results').addClass('d-none'); 1121 1122 $.ajax({ 1123 url: nhrotmOptionsTableManager.ajaxUrl, 1124 method: "POST", 1125 data: { 1126 action: "nhrotm_search_replace_execute", 1127 nonce: nhrotmOptionsTableManager.nonce, 1128 search: search, 1129 replace: replace, 1130 dry_run: dryRun 1131 }, 1132 success: function (response) { 1133 $('.nhrotm-search-replace-loading').addClass('d-none'); 1134 $('.nhrotm-search-replace-form').removeClass('d-none'); 1135 1136 if (response.success) { 1137 const data = response.data; 1138 const summary = `Found ${data.total_occurrences} occurrences in ${data.total_updated} options.` + 1139 (data.dry_run ? " (Preview Mode - No changes saved)" : " (Changes Saved)"); 1140 1141 $('#nhrotm-sr-summary-text').text(summary); 1142 1143 let html = ''; 1144 if (data.details.length === 0) { 1145 html = '<tr><td colspan="2">No matches found.</td></tr>'; 1146 } else { 1147 data.details.forEach(item => { 1148 const safeName = $('<div>').text(item.option_name).html(); 1149 html += `<tr> 1150 <td>${safeName}</td> 1151 <td>${item.occurrences}</td> 1152 </tr>`; 1153 }); 1154 } 1155 $('#nhrotm-sr-list-body').html(html); 1156 $('#nhrotm-search-replace-results').show(); 1157 $('#nhrotm-search-replace-results').removeClass('d-none'); // Ensure class doesn't interfere 1158 1159 showToast(data.dry_run ? "Search complete (Preview)" : "Search and replace complete!", "success"); 1160 1161 // Reload main table if changes were made 1162 if (!data.dry_run && $.fn.DataTable.isDataTable('#nhrotm-data-table')) { 1163 $('#nhrotm-data-table').DataTable().ajax.reload(null, false); 1164 } 1165 } else { 1166 showToast("Operation failed: " + response.data, "error"); 1167 } 1168 }, 1169 error: function () { 1170 $('.nhrotm-search-replace-loading').addClass('d-none'); 1171 $('.nhrotm-search-replace-form').removeClass('d-none'); 1172 showToast("Connection error", "error"); 1173 } 1174 }); 1175 }); 1176 1177 // --- Import / Export Feature --- 1178 1179 // Export Basket 1180 let exportBasket = new Set(); 1181 1182 // Search for options to export 1183 let searchTimeout; 1184 $('#nhrotm-export-search').on('input', function() { 1185 const term = $(this).val(); 1186 const $suggestions = $('#nhrotm-export-suggestions'); 1187 1188 clearTimeout(searchTimeout); 1189 1190 if (term.length < 2) { 1191 $suggestions.addClass('d-none').html(''); 1192 return; 1193 } 1194 1195 searchTimeout = setTimeout(function() { 1196 $.ajax({ 1197 url: nhrotmOptionsTableManager.ajaxUrl, 1198 method: 'GET', 1199 data: { 1200 action: 'nhrotm_search_options_for_export', 1201 nonce: nhrotmOptionsTableManager.nonce, 1202 term: term 1203 }, 1204 success: function(response) { 1205 if (response.success && response.data.length > 0) { 1206 let html = ''; 1207 response.data.forEach(opt => { 1208 html += `<div class="nhrotm-suggestion-item" data-option="${opt}">${opt}</div>`; 1209 }); 1210 $suggestions.html(html).removeClass('d-none'); 1211 } else { 1212 $suggestions.addClass('d-none'); 1213 } 1214 } 1215 }); 1216 }, 300); 1217 }); 1218 1219 // Add option to basket 1220 $(document).on('click', '.nhrotm-suggestion-item', function() { 1221 const option = $(this).data('option'); 1222 if (exportBasket.has(option)) return; 1223 1224 exportBasket.add(option); 1225 updateExportBasketUI(); 1226 1227 $('#nhrotm-export-search').val(''); 1228 $('#nhrotm-export-suggestions').addClass('d-none'); 1229 }); 1230 1231 // Remove from basket 1232 $(document).on('click', '.nhrotm-basket-remove', function() { 1233 const option = $(this).data('option'); 1234 exportBasket.delete(option); 1235 updateExportBasketUI(); 1236 }); 1237 1238 function updateExportBasketUI() { 1239 const $list = $('#nhrotm-basket-list'); 1240 const $count = $('#nhrotm-basket-count'); 1241 const $btn = $('#nhrotm-do-export'); 1242 1243 $count.text(exportBasket.size); 1244 1245 if (exportBasket.size === 0) { 1246 $list.html('<li class="empty-basket">No options selected.</li>'); 1247 $btn.prop('disabled', true); 1248 } else { 1249 let html = ''; 1250 exportBasket.forEach(opt => { 1251 html += `<li> 1252 ${opt} 1253 <span class="nhrotm-basket-remove dashicons dashicons-trash" data-option="${opt}" title="Remove"></span> 1254 </li>`; 1255 }); 1256 $list.html(html); 1257 $btn.prop('disabled', false); 1258 } 1259 } 1260 1261 // Execute Export 1262 $('#nhrotm-do-export').on('click', function() { 1263 const options = Array.from(exportBasket); 1264 const $btn = $(this); 1265 $btn.prop('disabled', true).text('Generating...'); 1266 1267 $.ajax({ 1268 url: nhrotmOptionsTableManager.ajaxUrl, 1269 method: 'POST', 1270 data: { 1271 action: 'nhrotm_export_options', 1272 nonce: nhrotmOptionsTableManager.nonce, 1273 options: options 1274 }, 1275 success: function(response) { 1276 $btn.prop('disabled', false).text('Export to JSON'); 1277 if (response.success) { 1278 // Download File 1279 const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(response.data, null, 2)); 1280 const downloadAnchorNode = document.createElement('a'); 1281 const date = new Date().toISOString().slice(0, 10); 1282 downloadAnchorNode.setAttribute("href", dataStr); 1283 downloadAnchorNode.setAttribute("download", `wp-options-export-${date}.json`); 1284 document.body.appendChild(downloadAnchorNode); // required for firefox 1285 downloadAnchorNode.click(); 1286 downloadAnchorNode.remove(); 1287 showToast("Export generated successfully!", "success"); 1288 } else { 1289 showToast("Export failed: " + response.data, "error"); 1290 } 1291 }, 1292 error: function() { 1293 $btn.prop('disabled', false).text('Export to JSON'); 1294 showToast("Export request failed.", "error"); 1295 } 1296 }); 1297 }); 1298 1299 // Import Preview 1300 $('#nhrotm-import-file').on('change', function() { 1301 const file = this.files[0]; 1302 if (!file) return; 1303 1304 const formData = new FormData(); 1305 formData.append('action', 'nhrotm_preview_import'); 1306 formData.append('nonce', nhrotmOptionsTableManager.nonce); 1307 formData.append('import_file', file); 1308 1309 $.ajax({ 1310 url: nhrotmOptionsTableManager.ajaxUrl, 1311 method: 'POST', 1312 data: formData, 1313 contentType: false, 1314 processData: false, 1315 success: function(response) { 1316 if (response.success) { 1317 const preview = response.data.preview; 1318 const rawData = response.data.raw_data; // Store this for final import 1319 1320 $('#nhrotm-import-total').text(preview.length); 1321 1322 let html = ''; 1323 preview.forEach(item => { 1324 const statusClass = item.status === 'modified' ? 'update' : (item.status === 'new' ? 'install-now' : 'none'); 1325 html += `<tr> 1326 <td class="check-column"><input type="checkbox" class="nhrotm-import-item-checkbox" value="${item.name}" checked></td> 1327 <td><strong>${item.name}</strong></td> 1328 <td><span class="nhrotm-status-badge ${item.status}">${item.status}</span></td> 1329 <td><code>${item.current_snippet || '-'}</code></td> 1330 </tr>`; 1331 }); 1332 1333 $('#nhrotm-import-preview-body').html(html); 1334 $('#nhrotm-import-preview-area').removeClass('d-none'); 1335 1336 // Store raw data in a hidden way for next step (or re-send file, but storing JSON is easier for now) 1337 $('#nhrotm-execute-import').data('rawData', JSON.stringify(rawData)); 1338 } else { 1339 showToast("Preview failed: " + response.data, "error"); 1340 $('#nhrotm-import-file').val(''); 1341 } 1342 } 1343 }); 1344 }); 1345 1346 // Execute Import 1347 $('#nhrotm-execute-import').on('click', function() { 1348 const rawData = $(this).data('rawData'); 1349 if (!rawData) return; 1350 1351 const selected = []; 1352 $('.nhrotm-import-item-checkbox:checked').each(function() { 1353 selected.push($(this).val()); 1354 }); 1355 1356 if (selected.length === 0) { 1357 showToast("No options selected for import.", "error"); 1358 return; 1359 } 1360 1361 if (!confirm(`Are you sure you want to import ${selected.length} options? This will overwrite existing values.`)) { 1362 return; 1363 } 1364 1365 const $btn = $(this); 1366 $btn.prop('disabled', true).text('Importing...'); 1367 1368 $.ajax({ 1369 url: nhrotmOptionsTableManager.ajaxUrl, 1370 method: 'POST', 1371 data: { 1372 action: 'nhrotm_execute_import', 1373 nonce: nhrotmOptionsTableManager.nonce, 1374 raw_data: rawData, 1375 selected_options: selected 1376 }, 1377 success: function(response) { 1378 $btn.prop('disabled', false).text('Execute Import'); 1379 if (response.success) { 1380 showToast(`Successfully imported ${response.data.count} options!`, "success"); 1381 $('#nhrotm-import-preview-area').addClass('d-none'); 1382 $('#nhrotm-import-file').val(''); 1383 } else { 1384 showToast("Import failed: " + response.data, "error"); 1385 } 1386 } 1387 }); 1388 }); 1389 1390 // Save History Settings 1391 $('#nhrotm-history-settings-form').on('submit', function(e) { 1392 e.preventDefault(); 1393 const days = $('#nhrotm_history_retention_days').val(); 1394 const $btn = $('#nhrotm-save-history-settings'); 1395 1396 $btn.prop('disabled', true).val('Saving...'); 1397 1398 $.ajax({ 1399 url: nhrotmOptionsTableManager.ajaxUrl, 1400 method: 'POST', 1401 data: { 1402 action: 'nhrotm_save_history_settings', 1403 nonce: nhrotmOptionsTableManager.nonce, 1404 days: days 1405 }, 1406 success: function(response) { 1407 $btn.prop('disabled', false).val('Save Changes'); 1408 if (response.success) { 1409 showToast(response.data, "success"); 1410 } else { 1411 showToast("Failed to save: " + response.data, "error"); 1412 } 1413 }, 1414 error: function() { 1415 $btn.prop('disabled', false).val('Save Changes'); 1416 showToast("Request failed.", "error"); 1417 } 1418 }); 1419 }); 1420 1421 // Prune History Now 1422 $('#nhrotm-prune-history-now').on('click', function() { 1423 if(!confirm('Are you sure you want to delete old history logs immediately?')) return; 1424 1425 const $btn = $(this); 1426 $btn.prop('disabled', true).text('Pruning...'); 1427 1428 $.ajax({ 1429 url: nhrotmOptionsTableManager.ajaxUrl, 1430 method: 'POST', 1431 data: { 1432 action: 'nhrotm_prune_history', 1433 nonce: nhrotmOptionsTableManager.nonce 1434 }, 1435 success: function(response) { 1436 $btn.prop('disabled', false).text('Prune Now'); 1437 if (response.success) { 1438 const count = response.data.deleted !== false ? response.data.deleted : 0; 1439 showToast(`Pruned ${count} old entries.`, "success"); 1440 } else { 1441 showToast("Prune failed: " + response.data, "error"); 1442 } 1443 }, 1444 error: function() { 1445 $btn.prop('disabled', false).text('Prune Now'); 1446 showToast("Request failed.", "error"); 1447 } 1448 }); 1449 }); 1450 1451 1016 1452 }); 1017 1453 })(jQuery); -
nhrrob-options-table-manager/tags/1.3.0/includes/Ajax/AjaxHandler.php
r3442179 r3450271 12 12 use Nhrotm\OptionsTableManager\Managers\WprmRatingsTableManager; 13 13 use Nhrotm\OptionsTableManager\Managers\OptimizationManager; 14 use Nhrotm\OptionsTableManager\Managers\ScannerManager; 15 use Nhrotm\OptionsTableManager\Managers\SearchReplaceManager; 16 use Nhrotm\OptionsTableManager\Managers\ImportExportManager; 14 17 15 18 class AjaxHandler … … 20 23 private $wprm_ratings_manager; 21 24 private $optimization_manager; 25 private $scanner_manager; 26 private $search_replace_manager; 27 private $import_export_manager; 22 28 protected $wpdb; 23 29 … … 29 35 $this->wprm_ratings_manager = new WprmRatingsTableManager(); 30 36 $this->optimization_manager = new OptimizationManager(); 37 $this->scanner_manager = new ScannerManager(); 38 $this->search_replace_manager = new SearchReplaceManager(); 39 $this->import_export_manager = new ImportExportManager(); 31 40 32 41 global $wpdb; … … 65 74 // Auto Cleanup 66 75 'nhrotm_update_auto_cleanup_setting' => 'update_auto_cleanup_setting', 76 // Orphan Scanner 77 'nhrotm_scan_orphans' => 'scan_orphans', 78 'nhrotm_delete_orphaned_prefix' => 'delete_orphaned_prefix', 79 // Search & Replace 80 'nhrotm_search_replace_preview' => 'search_replace_preview', 81 'nhrotm_search_replace_execute' => 'search_replace_execute', 82 // Import / Export 83 'nhrotm_search_options_for_export' => 'search_options_for_export', 84 'nhrotm_export_options' => 'export_options', 85 'nhrotm_preview_import' => 'preview_import', 86 'nhrotm_execute_import' => 'execute_import', 87 88 // History & Optimization 89 'nhrotm_save_history_settings' => 'save_history_settings', 90 'nhrotm_prune_history' => 'prune_history', 67 91 ]; 68 92 … … 293 317 294 318 try { 319 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'nhrotm-admin-nonce')) { 320 throw new \Exception('Invalid nonce'); 321 } 295 322 $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 20; 296 323 $data = $this->optimization_manager->get_heavy_autoload_options($limit); … … 312 339 313 340 try { 341 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 342 throw new \Exception('Invalid nonce'); 343 } 314 344 $result = $this->optimization_manager->toggle_autoload(); 315 345 if ($result) { … … 356 386 wp_send_json_success('Settings updated'); 357 387 } 388 389 public function scan_orphans() 390 { 391 try { 392 $data = $this->scanner_manager->scan_orphans(); 393 wp_send_json_success($data); 394 } catch (\Exception $e) { 395 wp_send_json_error($e->getMessage()); 396 } 397 } 398 399 public function delete_orphaned_prefix() 400 { 401 try { 402 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 403 throw new \Exception('Invalid nonce'); 404 } 405 406 // check if user has permission to delete options 407 if (!current_user_can('manage_options')) { 408 throw new \Exception('Unauthorized'); 409 } 410 411 $prefix = isset($_POST['prefix']) ? sanitize_text_field(wp_unslash($_POST['prefix'])) : ''; 412 if (empty($prefix)) { 413 throw new \Exception('Prefix is required'); 414 } 415 416 $count = $this->scanner_manager->delete_by_prefix($prefix); 417 wp_send_json_success(['message' => sprintf('%d options deleted successfully', $count)]); 418 } catch (\Exception $e) { 419 wp_send_json_error($e->getMessage()); 420 } 421 } 422 423 public function search_replace_preview() 424 { 425 try { 426 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'nhrotm-admin-nonce')) { 427 throw new \Exception('Invalid nonce'); 428 } 429 430 // permission check 431 if (!current_user_can('manage_options')) { 432 throw new \Exception('Unauthorized'); 433 } 434 435 $search = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : ''; 436 if (empty($search)) { 437 throw new \Exception('Search string is required'); 438 } 439 440 $data = $this->search_replace_manager->preview_search($search); 441 wp_send_json_success($data); 442 } catch (\Exception $e) { 443 wp_send_json_error($e->getMessage()); 444 } 445 } 446 447 public function search_replace_execute() 448 { 449 try { 450 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 451 throw new \Exception('Invalid nonce'); 452 } 453 454 if (!current_user_can('manage_options')) { 455 throw new \Exception('Unauthorized'); 456 } 457 458 $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : ''; 459 $replace = isset($_POST['replace']) ? sanitize_text_field(wp_unslash($_POST['replace'])) : ''; 460 $dry_run = isset($_POST['dry_run']) && $_POST['dry_run'] === 'true'; 461 462 if (empty($search)) { 463 throw new \Exception('Search string is required'); 464 } 465 466 $result = $this->search_replace_manager->execute_replace($search, $replace, $dry_run); 467 wp_send_json_success($result); 468 } catch (\Exception $e) { 469 wp_send_json_error($e->getMessage()); 470 } 471 } 472 473 public function search_options_for_export() 474 { 475 try { 476 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'nhrotm-admin-nonce')) { 477 throw new \Exception('Invalid nonce'); 478 } 479 if (!current_user_can('manage_options')) { 480 throw new \Exception('Unauthorized'); 481 } 482 $term = isset($_GET['term']) ? sanitize_text_field(wp_unslash($_GET['term'])) : ''; 483 $results = $this->import_export_manager->search_options_for_export($term); 484 wp_send_json_success($results); 485 } catch (\Exception $e) { 486 wp_send_json_error($e->getMessage()); 487 } 488 } 489 490 public function export_options() 491 { 492 try { 493 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 494 throw new \Exception('Invalid nonce'); 495 } 496 if (!current_user_can('manage_options')) { 497 throw new \Exception('Unauthorized'); 498 } 499 $options = isset($_POST['options']) ? array_map('sanitize_text_field', wp_unslash($_POST['options'])) : []; 500 $data = $this->import_export_manager->export_options($options); 501 wp_send_json_success($data); 502 } catch (\Exception $e) { 503 wp_send_json_error($e->getMessage()); 504 } 505 } 506 507 public function preview_import() 508 { 509 try { 510 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 511 throw new \Exception('Invalid nonce'); 512 } 513 if (empty($_FILES['import_file'])) { 514 throw new \Exception('No file uploaded'); 515 } 516 517 // check if user has permission to import options 518 if (!current_user_can('manage_options')) { 519 throw new \Exception('Unauthorized'); 520 } 521 522 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File path validated via is_uploaded_file 523 $tmp_name = isset($_FILES['import_file']['tmp_name']) ? $_FILES['import_file']['tmp_name'] : ''; 524 if (empty($tmp_name) || !is_uploaded_file($tmp_name)) { 525 throw new \Exception('Invalid file upload'); 526 } 527 528 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File path validated via is_uploaded_file 529 $file_content = file_get_contents(wp_unslash($_FILES['import_file']['tmp_name'])); 530 if (!$file_content) throw new \Exception('Failed to read file'); 531 532 $json_data = json_decode($file_content, true); 533 if (!$json_data) throw new \Exception('Invalid JSON format'); 534 535 $preview = $this->import_export_manager->preview_import($json_data); 536 537 // Return preview + pass full JSON back to client (or stash in transient) for diffing 538 // For simplicity in this step, we return the parsed JSON structure to client to hold in memory 539 wp_send_json_success(['preview' => $preview, 'raw_data' => $json_data]); 540 } catch (\Exception $e) { 541 wp_send_json_error($e->getMessage()); 542 } 543 } 544 545 public function execute_import() 546 { 547 try { 548 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 549 throw new \Exception('Invalid nonce'); 550 } 551 if (!current_user_can('manage_options')) { 552 throw new \Exception('Unauthorized'); 553 } 554 555 $raw_data = isset($_POST['raw_data']) ? json_decode(stripslashes(sanitize_text_field(wp_unslash($_POST['raw_data']))), true) : null; 556 $selected = isset($_POST['selected_options']) ? array_map('sanitize_text_field', wp_unslash($_POST['selected_options'])) : []; 557 558 if (!$raw_data) throw new \Exception('Missing import data'); 559 560 $count = $this->import_export_manager->execute_import($raw_data, $selected); 561 wp_send_json_success(['count' => $count]); 562 } catch (\Exception $e) { 563 wp_send_json_error($e->getMessage()); 564 } 565 } 566 567 public function save_history_settings() 568 { 569 try { 570 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 571 throw new \Exception('Invalid nonce'); 572 } 573 if (!current_user_can('manage_options')) { 574 throw new \Exception('Unauthorized'); 575 } 576 577 $days = isset($_POST['days']) ? intval($_POST['days']) : 30; 578 if ($days < 1) $days = 30; 579 580 update_option('nhrotm_history_retention_days', $days); 581 wp_send_json_success('Settings saved'); 582 } catch (\Exception $e) { 583 wp_send_json_error($e->getMessage()); 584 } 585 } 586 587 public function prune_history() 588 { 589 try { 590 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 591 throw new \Exception('Invalid nonce'); 592 } 593 if (!current_user_can('manage_options')) { 594 throw new \Exception('Unauthorized'); 595 } 596 597 $days = get_option('nhrotm_history_retention_days', 30); 598 599 $history_manager = new \Nhrotm\OptionsTableManager\Managers\HistoryManager(); 600 $deleted = $history_manager->prune_history($days); 601 602 wp_send_json_success(['deleted' => $deleted]); 603 } catch (\Exception $e) { 604 wp_send_json_error($e->getMessage()); 605 } 606 } 358 607 } -
nhrrob-options-table-manager/tags/1.3.0/includes/Managers/BetterPaymentTableManager.php
r3442179 r3450271 49 49 50 50 $where_sql = ''; 51 if (!empty($where_clauses)) { 52 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses); 53 } 54 55 // Count filtered records 56 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 57 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}better_payment {$where_sql}"; 58 51 59 if (!empty($search)) { 52 $search_like = '%' . $wpdb->esc_like($search) . '%'; 53 $where_parts = []; 54 foreach ($columns as $column) { 55 // Prepare each part individually to avoid spread operator and keep SQL literal-ish 56 $where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like); 57 } 58 $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')'; 60 $filtered_records = $this->wpdb->get_var( 61 $this->wpdb->prepare($filtered_records_sql, ...$search_params_final) 62 ); 63 } else { 64 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 59 65 } 60 61 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 62 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 63 64 $order_sql = " ORDER BY $order_column $order_direction"; 65 $data = $wpdb->get_results( 66 $wpdb->prepare( 67 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 68 $start, 69 $length 70 ), 71 ARRAY_A 72 ); 73 // phpcs:enable 74 66 67 // SQL for ordering 68 $order_sql = "ORDER BY {$order_column} {$order_direction}"; 69 70 // Get data with search, order, and pagination 71 $data_sql = "SELECT * FROM {$this->wpdb->prefix}better_payment {$where_sql} {$order_sql} LIMIT %d, %d"; 72 73 if (!empty($search)) { 74 $query_params = array_merge($search_params_final, [$start, $length]); 75 $data = $this->wpdb->get_results( 76 $this->wpdb->prepare($data_sql, ...$query_params), 77 ARRAY_A 78 ); 79 } else { 80 $data = $this->wpdb->get_results( 81 $this->wpdb->prepare($data_sql, $start, $length), 82 ARRAY_A 83 ); 84 } 85 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 86 75 87 // Wrap the option_value in the scrollable-cell div 76 88 foreach ($data as &$row) { -
nhrrob-options-table-manager/tags/1.3.0/includes/Managers/CommonTableManager.php
r3442179 r3450271 51 51 $total_records = $wpdb->get_var("SELECT COUNT(*) FROM $table"); 52 52 53 $where_sql = ''; 53 // Build WHERE clause for search conditions 54 $where_clauses = []; 55 56 // Global search 54 57 if (!empty($search)) { 55 $search_like = '%' . $wpdb->esc_like($search) . '%'; 56 $where_parts = []; 58 $search_like = '%' . $this->wpdb->esc_like($search) . '%'; 59 $search_params = []; 60 $search_sql_parts = []; 61 62 // Add search for each column 57 63 foreach ($columns as $column) { 58 // Prepare each part individually to avoid spread operator and keep SQL literal-ish59 $ where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like);64 $search_sql_parts[] = "{$column} LIKE %s"; 65 $search_params[] = $search_like; 60 66 } 61 $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')'; 67 68 $where_clauses[] = "(" . implode(' OR ', $search_sql_parts) . ")"; 69 $search_params_final = $search_params; 62 70 } 63 64 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 65 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 66 67 $order_sql = " ORDER BY $order_column $order_direction"; 68 $data = $wpdb->get_results( 69 $wpdb->prepare( 70 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 71 $start, 72 $length 73 ), 74 ARRAY_A 75 ); 76 // phpcs:enable 77 71 72 // Build WHERE clause parts 73 $where_parts = []; 74 if (!empty($search)) { 75 $search_like = '%' . $this->wpdb->esc_like($search) . '%'; 76 $search_sql_parts = []; 77 foreach ($columns as $column) { 78 $search_sql_parts[] = "{$column} LIKE %s"; 79 } 80 $where_parts[] = "(" . implode(' OR ', $search_sql_parts) . ")"; 81 } 82 83 // Count filtered records 84 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 85 if (!empty($search)) { 86 $where_clause = 'WHERE ' . implode(' AND ', $where_parts); 87 $filtered_records = $this->wpdb->get_var( 88 $this->wpdb->prepare( 89 "SELECT COUNT(*) FROM {$this->table_name} $where_clause", 90 ...$search_params_final 91 ) 92 ); 93 } else { 94 $filtered_records = $this->wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"); 95 } 96 97 // Get data with search, order, and pagination 98 if (!empty($search)) { 99 $where_clause = 'WHERE ' . implode(' AND ', $where_parts); 100 $query_params = array_merge($search_params_final, [$start, $length]); 101 $data = $this->wpdb->get_results( 102 $this->wpdb->prepare( 103 "SELECT * FROM {$this->table_name} $where_clause ORDER BY {$order_column} {$order_direction} LIMIT %d, %d", 104 ...$query_params 105 ), 106 ARRAY_A 107 ); 108 } else { 109 $data = $this->wpdb->get_results( 110 $this->wpdb->prepare( 111 "SELECT * FROM {$this->table_name} ORDER BY {$order_column} {$order_direction} LIMIT %d, %d", 112 $start, 113 $length 114 ), 115 ARRAY_A 116 ); 117 } 118 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 119 78 120 // Wrap the option_value in the scrollable-cell div 79 121 foreach ($data as &$row) { -
nhrrob-options-table-manager/tags/1.3.0/includes/Managers/HistoryManager.php
r3442179 r3450271 71 71 } 72 72 73 // phpcs: disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared73 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific history tracking 74 74 return $wpdb->insert( 75 75 $table, … … 95 95 { 96 96 global $wpdb; 97 $table = $this->table_name; 98 99 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 97 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 100 98 return $wpdb->get_results( 101 99 $wpdb->prepare( 102 "SELECT * FROM $tableWHERE option_name = %s ORDER BY performed_at DESC",100 "SELECT * FROM {$wpdb->prefix}nhrotm_option_history WHERE option_name = %s ORDER BY performed_at DESC", 103 101 sanitize_text_field(wp_unslash($option_name)) 104 102 ), … … 141 139 $table = $this->table_name; 142 140 143 // phpcs: disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared141 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 144 142 $record = $wpdb->get_row( 145 $wpdb->prepare( 146 "SELECT * FROM $table WHERE id = %d", 147 $history_id 148 ), 143 $wpdb->prepare("SELECT * FROM {$wpdb->prefix}nhrotm_option_history WHERE id = %d", $history_id), 149 144 ARRAY_A 150 145 ); … … 199 194 return 'Failed to restore option'; 200 195 } 196 197 /** 198 * Prune history logs older than X days 199 * 200 * @param int $days Number of days to retain 201 * @return int|false Number of rows deleted or false on error 202 */ 203 public function prune_history($days = 30) 204 { 205 global $wpdb; 206 $days = intval($days); 207 if ($days < 1) $days = 30; 208 209 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific deletion 210 return $wpdb->query( 211 $wpdb->prepare( 212 "DELETE FROM {$wpdb->prefix}nhrotm_option_history WHERE performed_at < DATE_SUB(NOW(), INTERVAL %d DAY)", 213 $days 214 ) 215 ); 216 } 201 217 } -
nhrrob-options-table-manager/tags/1.3.0/includes/Managers/OptimizationManager.php
r3442179 r3450271 58 58 $table = $this->table_name; 59 59 60 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter61 $results = $wpdb->get_results(62 $wpdb->prepare(63 "SELECT option_name, option_value, autoload, LENGTH(option_value) as size_bytes64 FROM $table65 WHERE autoload NOT IN ('off', 'no', 'false', '0', '')66 ORDER BY size_bytes DESC67 LIMIT %d",68 $limit69 ),70 ARRAY_A71 );72 // phpcs:enable60 // Query to get autoloaded options 61 // Broaden the search to catch 'yes', 'true', '1', 'on' by excluding known 'no' values 62 global $wpdb; 63 64 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 65 $results = $wpdb->get_results($wpdb->prepare( 66 "SELECT option_name, option_value, autoload, LENGTH(option_value) as size_bytes 67 FROM {$wpdb->options} 68 WHERE autoload NOT IN ('off', 'no', 'false', '0', '') 69 ORDER BY size_bytes DESC 70 LIMIT %d", 71 $limit 72 ), ARRAY_A); 73 73 74 74 return array_map(function ($row) { … … 136 136 { 137 137 global $wpdb; 138 $table = $this->table_name; 139 140 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 141 $bytes = $wpdb->get_var( 142 "SELECT SUM(LENGTH(option_value)) FROM $table WHERE autoload NOT IN ('no', 'false', '0', '')" 143 ); 144 // phpcs:enable 138 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 139 $bytes = $wpdb->get_var("SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options} WHERE autoload NOT IN ('no', 'false', '0', '')"); 145 140 return size_format($bytes ? $bytes : 0); 146 141 } -
nhrrob-options-table-manager/tags/1.3.0/includes/Managers/OptionsTableManager.php
r3442179 r3450271 141 141 142 142 // Count filtered records 143 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 144 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 143 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 144 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}options {$where_sql}"; 145 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 145 146 146 147 // SQL for ordering … … 148 149 149 150 // Get data with search, order, and pagination 150 $data = $wpdb->get_results( 151 $wpdb->prepare( 152 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 153 $start, 154 $length 155 ), 151 $data_sql = "SELECT * FROM {$this->wpdb->prefix}options {$where_sql} {$order_sql} LIMIT %d, %d"; 152 $data = $this->wpdb->get_results( 153 $this->wpdb->prepare($data_sql, $start, $length), 156 154 ARRAY_A 157 155 ); 158 // phpcs:enable 156 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 159 157 160 158 // Wrap the option_value in the scrollable-cell div … … 386 384 $this->validate_permissions(); 387 385 388 $option_names = isset($_POST['option_names']) ? array_map('sanitize_text_field', (array) wp_unslash($_POST['option_names'])) : []; 386 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in array_map below 387 $option_names = isset($_POST['option_names']) ? wp_unslash((array) $_POST['option_names']) : []; 388 $option_names = array_map('sanitize_text_field', $option_names); 389 389 390 390 if (empty($option_names)) { -
nhrrob-options-table-manager/tags/1.3.0/includes/Managers/UsermetaTableManager.php
r3442179 r3450271 54 54 $where_sql = $wpdb->prepare(" WHERE (meta_key LIKE %s OR meta_value LIKE %s)", $search_like, $search_like); 55 55 } 56 57 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 58 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 59 60 $order_sql = " ORDER BY $order_column $order_direction"; 61 $data = $wpdb->get_results( 62 $wpdb->prepare( 63 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 64 $start, 65 $length 66 ), 56 57 // Count filtered records 58 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 59 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}usermeta {$where_sql}"; 60 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 61 62 // SQL for ordering 63 $order_sql = "ORDER BY {$order_column} {$order_direction}"; 64 65 // Get data with search, order, and pagination 66 $data_sql = "SELECT * FROM {$this->wpdb->prefix}usermeta {$where_sql} {$order_sql} LIMIT %d, %d"; 67 $data = $this->wpdb->get_results( 68 $this->wpdb->prepare($data_sql, $start, $length), 67 69 ARRAY_A 68 70 ); 69 // phpcs:enable 70 71 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 72 71 73 // Wrap the option_value in the scrollable-cell div 72 74 foreach ($data as &$row) { -
nhrrob-options-table-manager/tags/1.3.0/includes/Managers/WprmRatingsTableManager.php
r3442179 r3450271 49 49 50 50 $where_sql = ''; 51 if (!empty($where_clauses)) { 52 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses); 53 } 54 55 // Count filtered records 56 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 57 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}wprm_ratings {$where_sql}"; 58 51 59 if (!empty($search)) { 52 $search_like = '%' . $wpdb->esc_like($search) . '%'; 53 $where_parts = []; 54 foreach ($columns as $column) { 55 // Prepare each part individually to avoid spread operator and keep SQL literal-ish 56 $where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like); 57 } 58 $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')'; 60 $filtered_records = $this->wpdb->get_var( 61 $this->wpdb->prepare($filtered_records_sql, ...$search_params_final) 62 ); 63 } else { 64 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 59 65 } 60 61 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 62 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 63 64 $order_sql = " ORDER BY $order_column $order_direction"; 65 $data = $wpdb->get_results( 66 $wpdb->prepare( 67 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 68 $start, 69 $length 70 ), 71 ARRAY_A 72 ); 73 // phpcs:enable 74 66 67 // SQL for ordering 68 $order_sql = "ORDER BY {$order_column} {$order_direction}"; 69 70 // Get data with search, order, and pagination 71 $data_sql = "SELECT * FROM {$this->wpdb->prefix}wprm_ratings {$where_sql} {$order_sql} LIMIT %d, %d"; 72 73 if (!empty($search)) { 74 $query_params = array_merge($search_params_final, [$start, $length]); 75 $data = $this->wpdb->get_results( 76 $this->wpdb->prepare($data_sql, ...$query_params), 77 ARRAY_A 78 ); 79 } else { 80 $data = $this->wpdb->get_results( 81 $this->wpdb->prepare($data_sql, $start, $length), 82 ARRAY_A 83 ); 84 } 85 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 86 75 87 // Wrap the option_value in the scrollable-cell div 76 88 foreach ($data as &$row) { -
nhrrob-options-table-manager/tags/1.3.0/includes/views/admin/settings/index.php
r3442179 r3450271 44 44 <button class="tablinks settings-tab" data-tab="nhrotm-settings-tab"> 45 45 <?php esc_html_e('Settings', 'nhrrob-options-table-manager'); ?> 46 </button> 47 <button class="tablinks scanner-tab" data-tab="nhrotm-scanner-tab"> 48 <?php esc_html_e('Orphan Scanner', 'nhrrob-options-table-manager'); ?> 49 </button> 50 <button class="tablinks search-replace-tab" data-tab="nhrotm-search-replace-tab"> 51 <?php esc_html_e('Search & Replace', 'nhrrob-options-table-manager'); ?> 52 </button> 53 <button class="tablinks import-export-tab" data-tab="nhrotm-import-export-tab"> 54 <?php esc_html_e('Import / Export', 'nhrrob-options-table-manager'); ?> 46 55 </button> 47 56 </div> … … 110 119 </div> 111 120 112 <div id="nhrotm-usermeta-tab" class="nhrotm-tab-content " style="display:none;">121 <div id="nhrotm-usermeta-tab" class="nhrotm-tab-content d-none"> 113 122 <table id="nhrotm-data-table-usermeta" class="nhrotm-data-table wp-list-table widefat fixed striped"> 114 123 <thead> … … 126 135 127 136 <?php if ($is_better_payment_installed): ?> 128 <div id="nhrotm-better-payment-tab" class="nhrotm-tab-content " style="display:none;">137 <div id="nhrotm-better-payment-tab" class="nhrotm-tab-content d-none"> 129 138 <table id="nhrotm-data-table-better_payment" class="nhrotm-data-table wp-list-table widefat fixed striped"> 130 139 <thead> … … 146 155 147 156 <?php if ($is_wp_recipe_maker_installed): ?> 148 <div id="nhrotm-wprm-ratings-tab" class="nhrotm-tab-content " style="display:none;">157 <div id="nhrotm-wprm-ratings-tab" class="nhrotm-tab-content d-none"> 149 158 <table id="nhrotm-data-table-wprm_ratings" class="nhrotm-data-table wp-list-table widefat fixed striped"> 150 159 <thead> … … 166 175 </div> 167 176 168 <div id="nhrotm-wprm-analytics-tab" class="nhrotm-tab-content " style="display:none;">177 <div id="nhrotm-wprm-analytics-tab" class="nhrotm-tab-content d-none"> 169 178 <table id="nhrotm-data-table-wprm_analytics" class="nhrotm-data-table wp-list-table widefat fixed striped"> 170 179 <thead> … … 185 194 </div> 186 195 187 <div id="nhrotm-wprm-changelog-tab" class="nhrotm-tab-content " style="display:none;">196 <div id="nhrotm-wprm-changelog-tab" class="nhrotm-tab-content d-none"> 188 197 <table id="nhrotm-data-table-wprm_changelog" class="nhrotm-data-table wp-list-table widefat fixed striped"> 189 198 <thead> … … 300 309 <!-- History Modal --> 301 310 <div class="nhrotm-history-modal is-hidden"> 302 <div class="nhrotm-modal-content " style="max-width: 800px; width: 90%;">311 <div class="nhrotm-modal-content nhrotm-modal-lg"> 303 312 <h2><?php esc_html_e('Option History', 'nhrrob-options-table-manager'); ?>: <span 304 313 class="nhrotm-history-option-name"></span></h2> 305 314 306 <div class="nhrotm-history-loading " style="display:none; text-align: center; padding: 20px;">315 <div class="nhrotm-history-loading nhrotm-loader-box d-none"> 307 316 Loading... 308 317 </div> 309 318 310 <div class="nhrotm-history-list-container " style="max-height: 500px; overflow-y: auto;">311 <table class="wp-list-table widefat fixed striped " style="width:100%">319 <div class="nhrotm-history-list-container nhrotm-scrollable-vh-50"> 320 <table class="wp-list-table widefat fixed striped w-full"> 312 321 <thead> 313 322 <tr> … … 325 334 </div> 326 335 327 <button class="button nhrotm-close-history-modal " style="margin-top: 15px;">Close</button>336 <button class="button nhrotm-close-history-modal mt-3">Close</button> 328 337 </div> 329 338 </div> 330 339 331 <div id="nhrotm-autoload-optimizer-tab" class="nhrotm-tab-content " style="display:none;">332 <div class="card " style="max-width: 100%; margin-top: 20px; padding: 20px;">340 <div id="nhrotm-autoload-optimizer-tab" class="nhrotm-tab-content d-none"> 341 <div class="card nhrotm-card-full"> 333 342 <h2>Autoload Health Check</h2> 334 343 <div class="nhrotm-autoload-stats"> … … 352 361 </tbody> 353 362 </table> 354 </div> 355 </div> 356 357 <div id="nhrotm-settings-tab" class="nhrotm-tab-content" style="display:none;"> 358 <div class="card" style="max-width: 100%; margin-top: 20px; padding: 20px;"> 363 364 <div class="nhrotm-history-retention-settings mt-5 nhrotm-section-divider"> 365 <h3><?php esc_html_e('History Retention', 'nhrrob-options-table-manager'); ?></h3> 366 <p><?php esc_html_e('To prevent the history log from growing too large, you can automatically delete old logs.', 'nhrrob-options-table-manager'); ?></p> 367 <form id="nhrotm-history-settings-form"> 368 <table class="form-table"> 369 <tr> 370 <th scope="row"><label for="nhrotm_history_retention_days"><?php esc_html_e('Keep logs for (days)', 'nhrrob-options-table-manager'); ?></label></th> 371 <td> 372 <input name="nhrotm_history_retention_days" type="number" id="nhrotm_history_retention_days" value="<?php echo esc_attr(get_option('nhrotm_history_retention_days', 30)); ?>" class="small-text" min="1"> 373 <p class="description"><?php esc_html_e('Logs older than this will be automatically deleted daily.', 'nhrrob-options-table-manager'); ?></p> 374 </td> 375 </tr> 376 </table> 377 <p class="submit"> 378 <input type="submit" name="submit" id="nhrotm-save-history-settings" class="button button-primary" value="<?php esc_attr_e('Save Changes', 'nhrrob-options-table-manager'); ?>"> 379 <button type="button" id="nhrotm-prune-history-now" class="button button-secondary"><?php esc_html_e('Prune Now', 'nhrrob-options-table-manager'); ?></button> 380 </p> 381 </form> 382 </div> 383 </div> 384 </div> 385 386 <div id="nhrotm-scanner-tab" class="nhrotm-tab-content d-none"> 387 <div class="card nhrotm-tab-card"> 388 <h2>Orphaned Options Scanner</h2> 389 <p class="description">Scan your database for options left behind by uninstalled or inactive plugins. Identifying these helps reduce database bloat.</p> 390 391 <div class="nhrotm-scanner-actions mb-4"> 392 <button id="nhrotm-start-scan" class="button button-primary">Start Deep Scan</button> 393 </div> 394 395 <div class="nhrotm-scanner-loading loader-container d-none"> 396 <span class="spinner is-active nhrotm-spinner-centered"></span> 397 <p>Analyzing database prefixes and cross-referencing with plugin directories...</p> 398 </div> 399 400 <div id="nhrotm-scanner-results" class="d-none"> 401 <table class="wp-list-table widefat fixed striped"> 402 <thead> 403 <tr> 404 <th>Prefix Candidate</th> 405 <th>Option Count</th> 406 <th>Likely Source</th> 407 <th>Risk Level</th> 408 <th>Action</th> 409 </tr> 410 </thead> 411 <tbody id="nhrotm-scanner-list-body"> 412 <!-- Scan results --> 413 </tbody> 414 </table> 415 </div> 416 417 <div id="nhrotm-scanner-empty" class="loader-container d-none"> 418 <p>No significant orphaned options detected. Your database looks clean!</p> 419 </div> 420 </div> 421 </div> 422 423 <div id="nhrotm-search-replace-tab" class="nhrotm-tab-content d-none"> 424 <div class="card nhrotm-tab-card"> 425 <h2>Global Search & Replace</h2> 426 <p class="description">Find and replace strings across your entire options table. Supports serialized data and JSON safely.</p> 427 428 <div class="nhrotm-search-replace-form mb-4 max-w-600"> 429 <div class="nhrotm-form-field mb-3"> 430 <label class="font-semibold d-block mb-1">Search for:</label> 431 <input type="text" id="nhrotm-search-string" class="regular-text" placeholder="e.g. old-domain.com"> 432 </div> 433 <div class="nhrotm-form-field mb-3"> 434 <label class="font-semibold d-block mb-1">Replace with:</label> 435 <input type="text" id="nhrotm-replace-string" class="regular-text" placeholder="e.g. new-domain.com"> 436 </div> 437 <div class="nhrotm-form-field mb-4"> 438 <label class="nhrotm-switch-label d-flex items-center gap-10 pointer"> 439 <input type="checkbox" id="nhrotm-dry-run-toggle" checked> 440 <span>Dry Run (Preview changes without saving)</span> 441 </label> 442 </div> 443 444 <div class="nhrotm-form-actions"> 445 <button type="button" id="nhrotm-search-replace-btn" class="button button-primary">Execute Replacement</button> 446 </div> 447 </div> 448 449 <div class="nhrotm-search-replace-loading loader-container d-none"> 450 <span class="spinner is-active nhrotm-spinner-centered"></span> 451 <p>Processing database records... This may take a moment.</p> 452 </div> 453 454 <div id="nhrotm-search-replace-results" class="mt-5 d-none"> 455 <div class="nhrotm-sr-summary nhrotm-summary-box"> 456 <p id="nhrotm-sr-summary-text" class="m-0 font-semibold"></p> 457 </div> 458 459 <table class="wp-list-table widefat fixed striped"> 460 <thead> 461 <tr> 462 <th>Option Name</th> 463 <th>Occurrences Found</th> 464 </tr> 465 </thead> 466 <tbody id="nhrotm-sr-list-body"> 467 <!-- Results --> 468 </tbody> 469 </table> 470 </div> 471 </div> 472 </div> 473 474 <div id="nhrotm-import-export-tab" class="nhrotm-tab-content d-none"> 475 <div class="card nhrotm-tab-card"> 476 <h2>Import / Export Options</h2> 477 <p class="description">Selectively export options or import a configuration file from another site.</p> 478 479 <div class="nhrotm-ie-columns d-flex gap-40 mt-30"> 480 <!-- Export Section --> 481 <div class="nhrotm-ie-column flex-1"> 482 <h3>Export Options</h3> 483 <p>Search and select options to add to your export basket.</p> 484 485 <div class="nhrotm-form-field mb-3"> 486 <input type="text" id="nhrotm-export-search" class="regular-text w-full" placeholder="Search option names..."> 487 <div id="nhrotm-export-suggestions" class="nhrotm-suggestions-box d-none"></div> 488 </div> 489 490 <div class="nhrotm-export-basket"> 491 <h4>Selected Options (<span id="nhrotm-basket-count">0</span>)</h4> 492 <ul id="nhrotm-basket-list"> 493 <li class="empty-basket">No options selected.</li> 494 </ul> 495 </div> 496 497 <button id="nhrotm-do-export" class="button button-primary mt-3" disabled>Export to JSON</button> 498 </div> 499 500 <!-- Import Section --> 501 <div class="nhrotm-ie-column flex-1 nhrotm-ie-border-left"> 502 <h3>Import Options</h3> 503 <p>Upload a previously exported JSON file.</p> 504 505 <div class="nhrotm-form-field mb-3"> 506 <input type="file" id="nhrotm-import-file" accept=".json"> 507 </div> 508 509 <div id="nhrotm-import-preview-area" class="d-none"> 510 <h4>Import Preview</h4> 511 <div class="nhrotm-import-stats mb-2"> 512 Found <span id="nhrotm-import-total">0</span> options. 513 </div> 514 515 <div class="nhrotm-scrollable-table nhrotm-scrollable-vh-30"> 516 <table class="wp-list-table widefat fixed striped"> 517 <thead> 518 <tr> 519 <td class="check-column"><input type="checkbox" id="nhrotm-import-select-all" checked></td> 520 <th>Option Name</th> 521 <th>Status</th> 522 <th>Current Val (Snippet)</th> 523 </tr> 524 </thead> 525 <tbody id="nhrotm-import-preview-body"></tbody> 526 </table> 527 </div> 528 529 <button id="nhrotm-execute-import" class="button button-primary mt-3">Execute Import</button> 530 </div> 531 </div> 532 </div> 533 534 </div> 535 </div> 536 537 <div id="nhrotm-settings-tab" class="nhrotm-tab-content d-none"> 538 <div class="card nhrotm-tab-card"> 359 539 <h2>Settings</h2> 360 540 <table class="form-table"> -
nhrrob-options-table-manager/tags/1.3.0/nhrrob-options-table-manager.php
r3442179 r3450271 6 6 * Author: Nazmul Hasan Robin 7 7 * Author URI: https://profiles.wordpress.org/nhrrob/ 8 * Version: 1. 2.08 * Version: 1.3.0 9 9 * Requires at least: 6.0 10 10 * Requires PHP: 7.4 … … 30 30 * @var string 31 31 */ 32 const nhrotm_version = '1. 1.9';32 const nhrotm_version = '1.3.0'; 33 33 34 34 /** … … 55 55 wp_schedule_event(time(), 'daily', 'nhrotm_daily_cleanup'); 56 56 } 57 58 if (!wp_next_scheduled('nhrotm_daily_history_prune')) { 59 wp_schedule_event(time(), 'daily', 'nhrotm_daily_history_prune'); 60 } 57 61 } 58 62 … … 63 67 { 64 68 wp_clear_scheduled_hook('nhrotm_daily_cleanup'); 69 wp_clear_scheduled_hook('nhrotm_daily_history_prune'); 65 70 } 66 71 … … 107 112 // Cron Handler 108 113 add_action('nhrotm_daily_cleanup', [$this, 'run_cleanup']); 114 add_action('nhrotm_daily_history_prune', [$this, 'run_history_prune']); 109 115 110 116 new Nhrotm\OptionsTableManager\Assets(); … … 112 118 if (defined('DOING_AJAX') && DOING_AJAX) { 113 119 new Nhrotm\OptionsTableManager\Ajax\AjaxHandler(); 120 } 121 122 if (defined('WP_CLI') && WP_CLI) { 123 \WP_CLI::add_command('nhr-options', '\Nhrotm\OptionsTableManager\Cli\CliCommands'); 114 124 } 115 125 … … 130 140 } 131 141 } 142 143 /** 144 * Run history pruning 145 */ 146 public function run_history_prune() 147 { 148 $days = get_option('nhrotm_history_retention_days', 30); 149 $history_manager = new \Nhrotm\OptionsTableManager\Managers\HistoryManager(); 150 $history_manager->prune_history($days); 151 } 132 152 } 133 153 -
nhrrob-options-table-manager/tags/1.3.0/readme.txt
r3442179 r3450271 1 1 === NHR Advanced Options Table Manager & Autoload Optimizer === 2 2 Contributors: nhrrob 3 Tags: wp_options, transients, usermeta, autoload-optimizer, database-optimization3 Tags: wp_options, transients, usermeta, optimize, database-optimization 4 4 Requires at least: 6.0 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.07 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 37 37 - **Live Search & Pagination** – High-performance DataTables with server-side processing. 38 38 - **Security & Optimization** – Protection for core WordPress options to prevent accidental data loss. 39 - **Import / Export** – Move settings between sites easily with JSON support. 40 - **Global Search & Replace** – Safely replace strings across the database with dry-run preview. 41 - **Orphan Scanner** – Find and clean up leftovers from uninstalled plugins. 42 - **WP-CLI Support** – Manage options (wp nhr-options list, wp nhr-options delete) from the command line. 39 43 40 44 ### 🚀 Coming Soon 41 45 We're constantly improving NHR Options Table Manager! Here's what's on the way: 42 - **Export Data** – Export your options and analytics to CSV or JSON formats.43 46 - **Scheduled Backups** – Automatically backup your `wp_options` table before major changes. 44 47 … … 74 77 75 78 **Can I delete expired transients?** 76 Not yet, but this feature is coming soon! 79 Yes! We have an automated daily cleanup feature and a manual delete button. 77 80 78 81 == Screenshots == … … 86 89 87 90 == Changelog == 91 92 = 1.3.0 - 30/01/2026 = 93 - Added: Export/Import feature allowing JSON configuration portability 94 - Added: Global Search & Replace utility with safe serialization handling and Dry Run mode 95 - Added: Orphaned Options Scanner to identifying bloat from uninstalled plugins 96 - Added: Option History Pruning (Automated via Cron & Manual control) 97 - Added: WP-CLI Support (`nhr-options list`, `nhr-options delete`) 88 98 89 99 = 1.2.0 - 19/01/2026 = -
nhrrob-options-table-manager/trunk/assets/css/admin.css
r3442179 r3450271 9 9 } 10 10 11 .d-block { 12 display: block; 13 } 14 11 15 .m-auto { 12 16 margin: auto; 13 17 } 14 18 19 .w-full { 20 width: 100%; 21 } 22 23 .flex-1 { 24 flex: 1; 25 } 26 27 .cursor-pointer, 28 .pointer { 29 cursor: pointer; 30 } 31 32 .max-w-600 { 33 max-width: 600px; 34 } 35 36 .d-flex { 37 display: flex; 38 } 39 40 .items-center { 41 align-items: center; 42 } 43 44 .gap-10 { 45 gap: 10px; 46 } 47 48 .gap-20 { 49 gap: 20px; 50 } 51 52 .gap-40 { 53 gap: 40px; 54 } 55 56 .font-semibold { 57 font-weight: 600; 58 } 59 60 .m-0 { 61 margin: 0; 62 } 63 15 64 .m-1 { 16 65 margin: 5px; … … 53 102 } 54 103 104 .mt-30 { 105 margin-top: 30px; 106 } 107 55 108 .mb-1 { 56 109 margin-bottom: 5px; … … 215 268 .text-center { 216 269 text-align: center; 270 } 271 272 .loader-container { 273 text-align: center; 274 padding: 40px; 275 } 276 277 .nhrotm-loader-box { 278 text-align: center; 279 padding: 20px; 280 } 281 282 .nhrotm-tab-card { 283 max-width: 100%; 284 margin-top: 20px; 285 padding: 20px; 286 } 287 288 .nhrotm-card-full { 289 max-width: 100%; 290 margin-top: 20px; 291 padding: 20px; 292 } 293 294 .nhrotm-section-divider { 295 border-top: 1px solid #ddd; 296 padding-top: 20px; 297 margin-top: 30px; 298 } 299 300 .nhrotm-sr-summary { 301 background: #f0f0f1; 302 padding: 15px; 303 border-radius: 4px; 304 margin-bottom: 20px; 217 305 } 218 306 … … 231 319 } 232 320 321 .nhrotm-scrollable-vh-50 { 322 max-height: 500px; 323 overflow-y: auto; 324 } 325 326 .nhrotm-scrollable-vh-30 { 327 max-height: 300px; 328 overflow-y: auto; 329 } 330 233 331 /* Modal design */ 234 332 .nhrotm-add-option-modal, … … 237 335 .nhrotm-history-modal { 238 336 display: none; 239 position: fixed; /* Stay in place */ 240 z-index: 1000; /* Sit on top */ 337 position: fixed; 338 /* Stay in place */ 339 z-index: 1000; 340 /* Sit on top */ 241 341 left: 0; 242 342 top: 0; 243 width: 100%; /* Full width */ 244 height: 100%; /* Full height */ 245 overflow: auto; /* Enable scroll if needed */ 246 background-color: rgba(0, 0, 0, 0.5); /* Black w/ opacity */ 343 width: 100%; 344 /* Full width */ 345 height: 100%; 346 /* Full height */ 347 overflow: auto; 348 /* Enable scroll if needed */ 349 background-color: rgba(0, 0, 0, 0.5); 350 /* Black w/ opacity */ 247 351 } 248 352 249 353 .nhrotm-modal-content { 250 354 background-color: #fefefe; 251 margin: 15% auto; /* 15% from the top and centered */ 355 margin: 15% auto; 356 /* 15% from the top and centered */ 252 357 padding: 20px; 253 358 border: 1px solid #888; 254 width: 80%; /* Could be more or less, depending on screen size */ 255 max-width: 500px; /* Set a max width for larger screens */ 256 border-radius: 8px; /* Rounded corners */ 359 width: 80%; 360 /* Could be more or less, depending on screen size */ 361 max-width: 500px; 362 /* Set a max width for larger screens */ 363 border-radius: 8px; 364 /* Rounded corners */ 365 } 366 367 .nhrotm-modal-lg { 368 max-width: 800px; 369 width: 90%; 257 370 } 258 371 … … 524 637 border-left: 4px solid #2271b1; 525 638 } 639 640 /* Orphan Scanner Styles */ 641 .nhrotm-risk-low { 642 background-color: #d4edda; 643 color: #155724; 644 padding: 2px 8px; 645 border-radius: 12px; 646 font-size: 11px; 647 font-weight: 600; 648 text-transform: uppercase; 649 } 650 651 .nhrotm-risk-medium { 652 background-color: #fff3cd; 653 color: #856404; 654 padding: 2px 8px; 655 border-radius: 12px; 656 font-size: 11px; 657 font-weight: 600; 658 text-transform: uppercase; 659 } 660 661 .nhrotm-risk-high { 662 background-color: #f8d7da; 663 color: #721c24; 664 padding: 2px 8px; 665 border-radius: 12px; 666 font-size: 11px; 667 font-weight: 600; 668 text-transform: uppercase; 669 } 670 671 .nhrotm-scanner-actions { 672 display: flex; 673 justify-content: center; 674 border-top: 1px solid #eee; 675 padding-top: 20px; 676 } 677 678 #nhrotm-scanner-results .wp-list-table { 679 margin-top: 20px; 680 } 681 682 #nhrotm-scanner-results td { 683 vertical-align: middle; 684 } 685 686 .nhrotm-spinner-centered { 687 float: none; 688 margin-bottom: 10px; 689 } 690 691 /* Search & Replace Styles */ 692 .nhrotm-search-replace-form .nhrotm-form-field input[type="text"] { 693 width: 100%; 694 max-width: 400px; 695 } 696 697 .nhrotm-summary-box { 698 border-left: 4px solid #2271b1; 699 } 700 701 .nhrotm-sr-list-body td { 702 vertical-align: middle; 703 } 704 705 /* Import / Export Styles */ 706 .nhrotm-ie-columns { 707 display: flex; 708 flex-wrap: wrap; 709 gap: 40px; 710 } 711 712 .nhrotm-ie-column { 713 min-width: 300px; 714 } 715 716 .nhrotm-ie-border-left { 717 border-left: 1px solid #ddd; 718 padding-left: 40px; 719 } 720 721 /* Export Suggestions */ 722 .nhrotm-suggestions-box { 723 border: 1px solid #ccd0d4; 724 max-height: 200px; 725 overflow-y: auto; 726 background: #fff; 727 position: absolute; 728 width: 95%; 729 /* Approximate relative width */ 730 z-index: 100; 731 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 732 } 733 734 .nhrotm-suggestion-item { 735 padding: 8px 12px; 736 cursor: pointer; 737 border-bottom: 1px solid #f0f0f1; 738 } 739 740 .nhrotm-suggestion-item:hover { 741 background: #f0f0f1; 742 color: #2271b1; 743 } 744 745 /* Export Basket */ 746 .nhrotm-export-basket { 747 background: #f6f7f7; 748 border: 1px solid #dcdcde; 749 padding: 15px; 750 border-radius: 4px; 751 min-height: 100px; 752 max-height: 300px; 753 overflow-y: auto; 754 } 755 756 .nhrotm-export-basket ul { 757 margin: 0; 758 padding: 0; 759 list-style: none; 760 } 761 762 .nhrotm-export-basket li { 763 background: #fff; 764 border: 1px solid #c3c4c7; 765 margin-bottom: 5px; 766 padding: 6px 10px; 767 border-radius: 3px; 768 display: flex; 769 justify-content: space-between; 770 align-items: center; 771 } 772 773 .nhrotm-basket-remove { 774 cursor: pointer; 775 color: #d63638; 776 } 777 778 .nhrotm-basket-remove:hover { 779 color: #b32d2e; 780 } 781 782 .empty-basket { 783 color: #646970; 784 font-style: italic; 785 background: transparent !important; 786 border: none !important; 787 padding: 0 !important; 788 } 789 790 /* Import Preview */ 791 .nhrotm-status-badge { 792 padding: 2px 8px; 793 border-radius: 12px; 794 font-size: 11px; 795 font-weight: 600; 796 text-transform: uppercase; 797 } 798 799 .nhrotm-status-badge.new { 800 background-color: #d4edda; 801 color: #155724; 802 } 803 804 .nhrotm-status-badge.modified, 805 .nhrotm-status-badge.changed { 806 background-color: #fff3cd; 807 color: #856404; 808 } 809 810 .nhrotm-status-badge.unchanged, 811 .nhrotm-status-badge.none { 812 background-color: #e2e4e7; 813 color: #646970; 814 } 815 816 .nhrotm-scrollable-table { 817 border: 1px solid #c3c4c7; 818 } 819 820 .nhrotm-scrollable-table .wp-list-table { 821 margin: 0; 822 border: none; 823 box-shadow: none; 824 } -
nhrrob-options-table-manager/trunk/assets/js/admin.js
r3442179 r3450271 849 849 850 850 // Handle Global UI Elements visibility 851 // Feature tabs don't show filters or "Add Option" 852 const isFeatureTab = $(this).hasClass('optimization-tab') || $(this).hasClass('settings-tab'); 853 851 852 // "Add New Option" button - Visible ONLY for "Options Table" 853 if ($(this).hasClass('options-table')) { 854 $('.nhrotm-add-option-button').show(); 855 } else { 856 $('.nhrotm-add-option-button').hide(); 857 } 858 859 // Filters and User ID - Hidden for Feature tabs 860 const isFeatureTab = $(this).hasClass('optimization-tab') || $(this).hasClass('settings-tab') || $(this).hasClass('scanner-tab') || $(this).hasClass('search-replace-tab') || $(this).hasClass('import-export-tab'); 861 854 862 if (isFeatureTab) { 855 863 $('.nhrotm-filter-container').hide(); 856 $('.nhrotm-add-option-button').hide();857 864 $('.logged-user-id').hide(); 858 865 } else { … … 860 867 $('.logged-user-id').show(); 861 868 862 // "Add Option" is specifically for the main Options Table863 if ($(this).hasClass('options-table')) {864 $('.nhrotm-add-option-button').show();865 } else {866 $('.nhrotm-add-option-button').hide();867 }868 869 869 // Adjust DataTables inside this tab automatically 870 870 $targetContainer.find('table.nhrotm-data-table').each(function () { … … 878 878 if ($(this).hasClass('optimization-tab')) { 879 879 loadAutoloadData(); 880 } else if ($(this).hasClass('scanner-tab')) { 881 // Potential initial load or reset view 882 } else if ($(this).hasClass('search-replace-tab')) { 883 // Potential reset view 884 $('#nhrotm-search-replace-results').hide(); 885 $('.nhrotm-search-replace-form').show(); 880 886 } 881 887 }); … … 1014 1020 }); 1015 1021 1022 // --- Orphan Scanner Feature --- 1023 1024 $('#nhrotm-start-scan').on('click', function () { 1025 $('.nhrotm-scanner-actions').addClass('d-none'); 1026 $('.nhrotm-scanner-loading').removeClass('d-none'); 1027 $('#nhrotm-scanner-results').addClass('d-none'); 1028 $('#nhrotm-scanner-empty').addClass('d-none'); 1029 1030 $.ajax({ 1031 url: nhrotmOptionsTableManager.ajaxUrl, 1032 method: "GET", 1033 data: { 1034 action: "nhrotm_scan_orphans", 1035 nonce: nhrotmOptionsTableManager.nonce 1036 }, 1037 success: function (response) { 1038 $('.nhrotm-scanner-loading').addClass('d-none'); 1039 $('.nhrotm-scanner-actions').removeClass('d-none'); 1040 1041 if (response.success) { 1042 const orphans = response.data; 1043 if (orphans.length === 0) { 1044 $('#nhrotm-scanner-empty').removeClass('d-none'); 1045 } else { 1046 let html = ''; 1047 orphans.forEach(item => { 1048 html += `<tr> 1049 <td><strong>${item.prefix}</strong></td> 1050 <td>${item.count}</td> 1051 <td>${item.possible_source}</td> 1052 <td><span class="nhrotm-risk-${item.risk.toLowerCase()}">${item.risk}</span></td> 1053 <td> 1054 <button class="button button-danger nhrotm-delete-orphans" data-prefix="${item.prefix}">Delete All</button> 1055 </td> 1056 </tr>`; 1057 }); 1058 $('#nhrotm-scanner-list-body').html(html); 1059 $('#nhrotm-scanner-results').removeClass('d-none'); 1060 } 1061 } else { 1062 showToast("Scan failed: " + response.data, "error"); 1063 } 1064 } 1065 }); 1066 }); 1067 1068 $(document).on('click', '.nhrotm-delete-orphans', function () { 1069 const prefix = $(this).data('prefix'); 1070 1071 if (confirm(`Are you sure you want to delete all options starting with "${prefix}"? This action cannot be undone.`)) { 1072 const $btn = $(this); 1073 $btn.prop('disabled', true).text('Deleting...'); 1074 1075 $.ajax({ 1076 url: nhrotmOptionsTableManager.ajaxUrl, 1077 method: "POST", 1078 data: { 1079 action: "nhrotm_delete_orphaned_prefix", 1080 nonce: nhrotmOptionsTableManager.nonce, 1081 prefix: prefix 1082 }, 1083 success: function (response) { 1084 if (response.success) { 1085 showToast(response.data.message, "success"); 1086 // Refresh scan 1087 $('#nhrotm-start-scan').trigger('click'); 1088 // Also reload main table if it's there 1089 if ($.fn.DataTable.isDataTable('#nhrotm-data-table')) { 1090 $('#nhrotm-data-table').DataTable().ajax.reload(null, false); 1091 } 1092 } else { 1093 showToast("Delete failed: " + response.data, "error"); 1094 $btn.prop('disabled', false).text('Delete All'); 1095 } 1096 } 1097 }); 1098 } 1099 }); 1100 1101 // --- Search & Replace Feature --- 1102 1103 $('#nhrotm-search-replace-btn').on('click', function (e) { 1104 e.preventDefault(); 1105 const search = $('#nhrotm-search-string').val(); 1106 const replace = $('#nhrotm-replace-string').val(); 1107 const dryRun = $('#nhrotm-dry-run-toggle').is(':checked'); 1108 1109 if (!search) { 1110 showToast("Search string is required", "error"); 1111 return; 1112 } 1113 1114 if (!dryRun && !confirm("WARNING: This will permanently modify your database records. Are you sure you want to proceed?")) { 1115 return; 1116 } 1117 1118 $('.nhrotm-search-replace-form').addClass('d-none'); 1119 $('.nhrotm-search-replace-loading').removeClass('d-none'); 1120 $('#nhrotm-search-replace-results').addClass('d-none'); 1121 1122 $.ajax({ 1123 url: nhrotmOptionsTableManager.ajaxUrl, 1124 method: "POST", 1125 data: { 1126 action: "nhrotm_search_replace_execute", 1127 nonce: nhrotmOptionsTableManager.nonce, 1128 search: search, 1129 replace: replace, 1130 dry_run: dryRun 1131 }, 1132 success: function (response) { 1133 $('.nhrotm-search-replace-loading').addClass('d-none'); 1134 $('.nhrotm-search-replace-form').removeClass('d-none'); 1135 1136 if (response.success) { 1137 const data = response.data; 1138 const summary = `Found ${data.total_occurrences} occurrences in ${data.total_updated} options.` + 1139 (data.dry_run ? " (Preview Mode - No changes saved)" : " (Changes Saved)"); 1140 1141 $('#nhrotm-sr-summary-text').text(summary); 1142 1143 let html = ''; 1144 if (data.details.length === 0) { 1145 html = '<tr><td colspan="2">No matches found.</td></tr>'; 1146 } else { 1147 data.details.forEach(item => { 1148 const safeName = $('<div>').text(item.option_name).html(); 1149 html += `<tr> 1150 <td>${safeName}</td> 1151 <td>${item.occurrences}</td> 1152 </tr>`; 1153 }); 1154 } 1155 $('#nhrotm-sr-list-body').html(html); 1156 $('#nhrotm-search-replace-results').show(); 1157 $('#nhrotm-search-replace-results').removeClass('d-none'); // Ensure class doesn't interfere 1158 1159 showToast(data.dry_run ? "Search complete (Preview)" : "Search and replace complete!", "success"); 1160 1161 // Reload main table if changes were made 1162 if (!data.dry_run && $.fn.DataTable.isDataTable('#nhrotm-data-table')) { 1163 $('#nhrotm-data-table').DataTable().ajax.reload(null, false); 1164 } 1165 } else { 1166 showToast("Operation failed: " + response.data, "error"); 1167 } 1168 }, 1169 error: function () { 1170 $('.nhrotm-search-replace-loading').addClass('d-none'); 1171 $('.nhrotm-search-replace-form').removeClass('d-none'); 1172 showToast("Connection error", "error"); 1173 } 1174 }); 1175 }); 1176 1177 // --- Import / Export Feature --- 1178 1179 // Export Basket 1180 let exportBasket = new Set(); 1181 1182 // Search for options to export 1183 let searchTimeout; 1184 $('#nhrotm-export-search').on('input', function() { 1185 const term = $(this).val(); 1186 const $suggestions = $('#nhrotm-export-suggestions'); 1187 1188 clearTimeout(searchTimeout); 1189 1190 if (term.length < 2) { 1191 $suggestions.addClass('d-none').html(''); 1192 return; 1193 } 1194 1195 searchTimeout = setTimeout(function() { 1196 $.ajax({ 1197 url: nhrotmOptionsTableManager.ajaxUrl, 1198 method: 'GET', 1199 data: { 1200 action: 'nhrotm_search_options_for_export', 1201 nonce: nhrotmOptionsTableManager.nonce, 1202 term: term 1203 }, 1204 success: function(response) { 1205 if (response.success && response.data.length > 0) { 1206 let html = ''; 1207 response.data.forEach(opt => { 1208 html += `<div class="nhrotm-suggestion-item" data-option="${opt}">${opt}</div>`; 1209 }); 1210 $suggestions.html(html).removeClass('d-none'); 1211 } else { 1212 $suggestions.addClass('d-none'); 1213 } 1214 } 1215 }); 1216 }, 300); 1217 }); 1218 1219 // Add option to basket 1220 $(document).on('click', '.nhrotm-suggestion-item', function() { 1221 const option = $(this).data('option'); 1222 if (exportBasket.has(option)) return; 1223 1224 exportBasket.add(option); 1225 updateExportBasketUI(); 1226 1227 $('#nhrotm-export-search').val(''); 1228 $('#nhrotm-export-suggestions').addClass('d-none'); 1229 }); 1230 1231 // Remove from basket 1232 $(document).on('click', '.nhrotm-basket-remove', function() { 1233 const option = $(this).data('option'); 1234 exportBasket.delete(option); 1235 updateExportBasketUI(); 1236 }); 1237 1238 function updateExportBasketUI() { 1239 const $list = $('#nhrotm-basket-list'); 1240 const $count = $('#nhrotm-basket-count'); 1241 const $btn = $('#nhrotm-do-export'); 1242 1243 $count.text(exportBasket.size); 1244 1245 if (exportBasket.size === 0) { 1246 $list.html('<li class="empty-basket">No options selected.</li>'); 1247 $btn.prop('disabled', true); 1248 } else { 1249 let html = ''; 1250 exportBasket.forEach(opt => { 1251 html += `<li> 1252 ${opt} 1253 <span class="nhrotm-basket-remove dashicons dashicons-trash" data-option="${opt}" title="Remove"></span> 1254 </li>`; 1255 }); 1256 $list.html(html); 1257 $btn.prop('disabled', false); 1258 } 1259 } 1260 1261 // Execute Export 1262 $('#nhrotm-do-export').on('click', function() { 1263 const options = Array.from(exportBasket); 1264 const $btn = $(this); 1265 $btn.prop('disabled', true).text('Generating...'); 1266 1267 $.ajax({ 1268 url: nhrotmOptionsTableManager.ajaxUrl, 1269 method: 'POST', 1270 data: { 1271 action: 'nhrotm_export_options', 1272 nonce: nhrotmOptionsTableManager.nonce, 1273 options: options 1274 }, 1275 success: function(response) { 1276 $btn.prop('disabled', false).text('Export to JSON'); 1277 if (response.success) { 1278 // Download File 1279 const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(response.data, null, 2)); 1280 const downloadAnchorNode = document.createElement('a'); 1281 const date = new Date().toISOString().slice(0, 10); 1282 downloadAnchorNode.setAttribute("href", dataStr); 1283 downloadAnchorNode.setAttribute("download", `wp-options-export-${date}.json`); 1284 document.body.appendChild(downloadAnchorNode); // required for firefox 1285 downloadAnchorNode.click(); 1286 downloadAnchorNode.remove(); 1287 showToast("Export generated successfully!", "success"); 1288 } else { 1289 showToast("Export failed: " + response.data, "error"); 1290 } 1291 }, 1292 error: function() { 1293 $btn.prop('disabled', false).text('Export to JSON'); 1294 showToast("Export request failed.", "error"); 1295 } 1296 }); 1297 }); 1298 1299 // Import Preview 1300 $('#nhrotm-import-file').on('change', function() { 1301 const file = this.files[0]; 1302 if (!file) return; 1303 1304 const formData = new FormData(); 1305 formData.append('action', 'nhrotm_preview_import'); 1306 formData.append('nonce', nhrotmOptionsTableManager.nonce); 1307 formData.append('import_file', file); 1308 1309 $.ajax({ 1310 url: nhrotmOptionsTableManager.ajaxUrl, 1311 method: 'POST', 1312 data: formData, 1313 contentType: false, 1314 processData: false, 1315 success: function(response) { 1316 if (response.success) { 1317 const preview = response.data.preview; 1318 const rawData = response.data.raw_data; // Store this for final import 1319 1320 $('#nhrotm-import-total').text(preview.length); 1321 1322 let html = ''; 1323 preview.forEach(item => { 1324 const statusClass = item.status === 'modified' ? 'update' : (item.status === 'new' ? 'install-now' : 'none'); 1325 html += `<tr> 1326 <td class="check-column"><input type="checkbox" class="nhrotm-import-item-checkbox" value="${item.name}" checked></td> 1327 <td><strong>${item.name}</strong></td> 1328 <td><span class="nhrotm-status-badge ${item.status}">${item.status}</span></td> 1329 <td><code>${item.current_snippet || '-'}</code></td> 1330 </tr>`; 1331 }); 1332 1333 $('#nhrotm-import-preview-body').html(html); 1334 $('#nhrotm-import-preview-area').removeClass('d-none'); 1335 1336 // Store raw data in a hidden way for next step (or re-send file, but storing JSON is easier for now) 1337 $('#nhrotm-execute-import').data('rawData', JSON.stringify(rawData)); 1338 } else { 1339 showToast("Preview failed: " + response.data, "error"); 1340 $('#nhrotm-import-file').val(''); 1341 } 1342 } 1343 }); 1344 }); 1345 1346 // Execute Import 1347 $('#nhrotm-execute-import').on('click', function() { 1348 const rawData = $(this).data('rawData'); 1349 if (!rawData) return; 1350 1351 const selected = []; 1352 $('.nhrotm-import-item-checkbox:checked').each(function() { 1353 selected.push($(this).val()); 1354 }); 1355 1356 if (selected.length === 0) { 1357 showToast("No options selected for import.", "error"); 1358 return; 1359 } 1360 1361 if (!confirm(`Are you sure you want to import ${selected.length} options? This will overwrite existing values.`)) { 1362 return; 1363 } 1364 1365 const $btn = $(this); 1366 $btn.prop('disabled', true).text('Importing...'); 1367 1368 $.ajax({ 1369 url: nhrotmOptionsTableManager.ajaxUrl, 1370 method: 'POST', 1371 data: { 1372 action: 'nhrotm_execute_import', 1373 nonce: nhrotmOptionsTableManager.nonce, 1374 raw_data: rawData, 1375 selected_options: selected 1376 }, 1377 success: function(response) { 1378 $btn.prop('disabled', false).text('Execute Import'); 1379 if (response.success) { 1380 showToast(`Successfully imported ${response.data.count} options!`, "success"); 1381 $('#nhrotm-import-preview-area').addClass('d-none'); 1382 $('#nhrotm-import-file').val(''); 1383 } else { 1384 showToast("Import failed: " + response.data, "error"); 1385 } 1386 } 1387 }); 1388 }); 1389 1390 // Save History Settings 1391 $('#nhrotm-history-settings-form').on('submit', function(e) { 1392 e.preventDefault(); 1393 const days = $('#nhrotm_history_retention_days').val(); 1394 const $btn = $('#nhrotm-save-history-settings'); 1395 1396 $btn.prop('disabled', true).val('Saving...'); 1397 1398 $.ajax({ 1399 url: nhrotmOptionsTableManager.ajaxUrl, 1400 method: 'POST', 1401 data: { 1402 action: 'nhrotm_save_history_settings', 1403 nonce: nhrotmOptionsTableManager.nonce, 1404 days: days 1405 }, 1406 success: function(response) { 1407 $btn.prop('disabled', false).val('Save Changes'); 1408 if (response.success) { 1409 showToast(response.data, "success"); 1410 } else { 1411 showToast("Failed to save: " + response.data, "error"); 1412 } 1413 }, 1414 error: function() { 1415 $btn.prop('disabled', false).val('Save Changes'); 1416 showToast("Request failed.", "error"); 1417 } 1418 }); 1419 }); 1420 1421 // Prune History Now 1422 $('#nhrotm-prune-history-now').on('click', function() { 1423 if(!confirm('Are you sure you want to delete old history logs immediately?')) return; 1424 1425 const $btn = $(this); 1426 $btn.prop('disabled', true).text('Pruning...'); 1427 1428 $.ajax({ 1429 url: nhrotmOptionsTableManager.ajaxUrl, 1430 method: 'POST', 1431 data: { 1432 action: 'nhrotm_prune_history', 1433 nonce: nhrotmOptionsTableManager.nonce 1434 }, 1435 success: function(response) { 1436 $btn.prop('disabled', false).text('Prune Now'); 1437 if (response.success) { 1438 const count = response.data.deleted !== false ? response.data.deleted : 0; 1439 showToast(`Pruned ${count} old entries.`, "success"); 1440 } else { 1441 showToast("Prune failed: " + response.data, "error"); 1442 } 1443 }, 1444 error: function() { 1445 $btn.prop('disabled', false).text('Prune Now'); 1446 showToast("Request failed.", "error"); 1447 } 1448 }); 1449 }); 1450 1451 1016 1452 }); 1017 1453 })(jQuery); -
nhrrob-options-table-manager/trunk/includes/Ajax/AjaxHandler.php
r3442179 r3450271 12 12 use Nhrotm\OptionsTableManager\Managers\WprmRatingsTableManager; 13 13 use Nhrotm\OptionsTableManager\Managers\OptimizationManager; 14 use Nhrotm\OptionsTableManager\Managers\ScannerManager; 15 use Nhrotm\OptionsTableManager\Managers\SearchReplaceManager; 16 use Nhrotm\OptionsTableManager\Managers\ImportExportManager; 14 17 15 18 class AjaxHandler … … 20 23 private $wprm_ratings_manager; 21 24 private $optimization_manager; 25 private $scanner_manager; 26 private $search_replace_manager; 27 private $import_export_manager; 22 28 protected $wpdb; 23 29 … … 29 35 $this->wprm_ratings_manager = new WprmRatingsTableManager(); 30 36 $this->optimization_manager = new OptimizationManager(); 37 $this->scanner_manager = new ScannerManager(); 38 $this->search_replace_manager = new SearchReplaceManager(); 39 $this->import_export_manager = new ImportExportManager(); 31 40 32 41 global $wpdb; … … 65 74 // Auto Cleanup 66 75 'nhrotm_update_auto_cleanup_setting' => 'update_auto_cleanup_setting', 76 // Orphan Scanner 77 'nhrotm_scan_orphans' => 'scan_orphans', 78 'nhrotm_delete_orphaned_prefix' => 'delete_orphaned_prefix', 79 // Search & Replace 80 'nhrotm_search_replace_preview' => 'search_replace_preview', 81 'nhrotm_search_replace_execute' => 'search_replace_execute', 82 // Import / Export 83 'nhrotm_search_options_for_export' => 'search_options_for_export', 84 'nhrotm_export_options' => 'export_options', 85 'nhrotm_preview_import' => 'preview_import', 86 'nhrotm_execute_import' => 'execute_import', 87 88 // History & Optimization 89 'nhrotm_save_history_settings' => 'save_history_settings', 90 'nhrotm_prune_history' => 'prune_history', 67 91 ]; 68 92 … … 293 317 294 318 try { 319 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'nhrotm-admin-nonce')) { 320 throw new \Exception('Invalid nonce'); 321 } 295 322 $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 20; 296 323 $data = $this->optimization_manager->get_heavy_autoload_options($limit); … … 312 339 313 340 try { 341 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 342 throw new \Exception('Invalid nonce'); 343 } 314 344 $result = $this->optimization_manager->toggle_autoload(); 315 345 if ($result) { … … 356 386 wp_send_json_success('Settings updated'); 357 387 } 388 389 public function scan_orphans() 390 { 391 try { 392 $data = $this->scanner_manager->scan_orphans(); 393 wp_send_json_success($data); 394 } catch (\Exception $e) { 395 wp_send_json_error($e->getMessage()); 396 } 397 } 398 399 public function delete_orphaned_prefix() 400 { 401 try { 402 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 403 throw new \Exception('Invalid nonce'); 404 } 405 406 // check if user has permission to delete options 407 if (!current_user_can('manage_options')) { 408 throw new \Exception('Unauthorized'); 409 } 410 411 $prefix = isset($_POST['prefix']) ? sanitize_text_field(wp_unslash($_POST['prefix'])) : ''; 412 if (empty($prefix)) { 413 throw new \Exception('Prefix is required'); 414 } 415 416 $count = $this->scanner_manager->delete_by_prefix($prefix); 417 wp_send_json_success(['message' => sprintf('%d options deleted successfully', $count)]); 418 } catch (\Exception $e) { 419 wp_send_json_error($e->getMessage()); 420 } 421 } 422 423 public function search_replace_preview() 424 { 425 try { 426 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'nhrotm-admin-nonce')) { 427 throw new \Exception('Invalid nonce'); 428 } 429 430 // permission check 431 if (!current_user_can('manage_options')) { 432 throw new \Exception('Unauthorized'); 433 } 434 435 $search = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : ''; 436 if (empty($search)) { 437 throw new \Exception('Search string is required'); 438 } 439 440 $data = $this->search_replace_manager->preview_search($search); 441 wp_send_json_success($data); 442 } catch (\Exception $e) { 443 wp_send_json_error($e->getMessage()); 444 } 445 } 446 447 public function search_replace_execute() 448 { 449 try { 450 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 451 throw new \Exception('Invalid nonce'); 452 } 453 454 if (!current_user_can('manage_options')) { 455 throw new \Exception('Unauthorized'); 456 } 457 458 $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : ''; 459 $replace = isset($_POST['replace']) ? sanitize_text_field(wp_unslash($_POST['replace'])) : ''; 460 $dry_run = isset($_POST['dry_run']) && $_POST['dry_run'] === 'true'; 461 462 if (empty($search)) { 463 throw new \Exception('Search string is required'); 464 } 465 466 $result = $this->search_replace_manager->execute_replace($search, $replace, $dry_run); 467 wp_send_json_success($result); 468 } catch (\Exception $e) { 469 wp_send_json_error($e->getMessage()); 470 } 471 } 472 473 public function search_options_for_export() 474 { 475 try { 476 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'nhrotm-admin-nonce')) { 477 throw new \Exception('Invalid nonce'); 478 } 479 if (!current_user_can('manage_options')) { 480 throw new \Exception('Unauthorized'); 481 } 482 $term = isset($_GET['term']) ? sanitize_text_field(wp_unslash($_GET['term'])) : ''; 483 $results = $this->import_export_manager->search_options_for_export($term); 484 wp_send_json_success($results); 485 } catch (\Exception $e) { 486 wp_send_json_error($e->getMessage()); 487 } 488 } 489 490 public function export_options() 491 { 492 try { 493 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 494 throw new \Exception('Invalid nonce'); 495 } 496 if (!current_user_can('manage_options')) { 497 throw new \Exception('Unauthorized'); 498 } 499 $options = isset($_POST['options']) ? array_map('sanitize_text_field', wp_unslash($_POST['options'])) : []; 500 $data = $this->import_export_manager->export_options($options); 501 wp_send_json_success($data); 502 } catch (\Exception $e) { 503 wp_send_json_error($e->getMessage()); 504 } 505 } 506 507 public function preview_import() 508 { 509 try { 510 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 511 throw new \Exception('Invalid nonce'); 512 } 513 if (empty($_FILES['import_file'])) { 514 throw new \Exception('No file uploaded'); 515 } 516 517 // check if user has permission to import options 518 if (!current_user_can('manage_options')) { 519 throw new \Exception('Unauthorized'); 520 } 521 522 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File path validated via is_uploaded_file 523 $tmp_name = isset($_FILES['import_file']['tmp_name']) ? $_FILES['import_file']['tmp_name'] : ''; 524 if (empty($tmp_name) || !is_uploaded_file($tmp_name)) { 525 throw new \Exception('Invalid file upload'); 526 } 527 528 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File path validated via is_uploaded_file 529 $file_content = file_get_contents(wp_unslash($_FILES['import_file']['tmp_name'])); 530 if (!$file_content) throw new \Exception('Failed to read file'); 531 532 $json_data = json_decode($file_content, true); 533 if (!$json_data) throw new \Exception('Invalid JSON format'); 534 535 $preview = $this->import_export_manager->preview_import($json_data); 536 537 // Return preview + pass full JSON back to client (or stash in transient) for diffing 538 // For simplicity in this step, we return the parsed JSON structure to client to hold in memory 539 wp_send_json_success(['preview' => $preview, 'raw_data' => $json_data]); 540 } catch (\Exception $e) { 541 wp_send_json_error($e->getMessage()); 542 } 543 } 544 545 public function execute_import() 546 { 547 try { 548 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 549 throw new \Exception('Invalid nonce'); 550 } 551 if (!current_user_can('manage_options')) { 552 throw new \Exception('Unauthorized'); 553 } 554 555 $raw_data = isset($_POST['raw_data']) ? json_decode(stripslashes(sanitize_text_field(wp_unslash($_POST['raw_data']))), true) : null; 556 $selected = isset($_POST['selected_options']) ? array_map('sanitize_text_field', wp_unslash($_POST['selected_options'])) : []; 557 558 if (!$raw_data) throw new \Exception('Missing import data'); 559 560 $count = $this->import_export_manager->execute_import($raw_data, $selected); 561 wp_send_json_success(['count' => $count]); 562 } catch (\Exception $e) { 563 wp_send_json_error($e->getMessage()); 564 } 565 } 566 567 public function save_history_settings() 568 { 569 try { 570 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 571 throw new \Exception('Invalid nonce'); 572 } 573 if (!current_user_can('manage_options')) { 574 throw new \Exception('Unauthorized'); 575 } 576 577 $days = isset($_POST['days']) ? intval($_POST['days']) : 30; 578 if ($days < 1) $days = 30; 579 580 update_option('nhrotm_history_retention_days', $days); 581 wp_send_json_success('Settings saved'); 582 } catch (\Exception $e) { 583 wp_send_json_error($e->getMessage()); 584 } 585 } 586 587 public function prune_history() 588 { 589 try { 590 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nhrotm-admin-nonce')) { 591 throw new \Exception('Invalid nonce'); 592 } 593 if (!current_user_can('manage_options')) { 594 throw new \Exception('Unauthorized'); 595 } 596 597 $days = get_option('nhrotm_history_retention_days', 30); 598 599 $history_manager = new \Nhrotm\OptionsTableManager\Managers\HistoryManager(); 600 $deleted = $history_manager->prune_history($days); 601 602 wp_send_json_success(['deleted' => $deleted]); 603 } catch (\Exception $e) { 604 wp_send_json_error($e->getMessage()); 605 } 606 } 358 607 } -
nhrrob-options-table-manager/trunk/includes/Managers/BetterPaymentTableManager.php
r3442179 r3450271 49 49 50 50 $where_sql = ''; 51 if (!empty($where_clauses)) { 52 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses); 53 } 54 55 // Count filtered records 56 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 57 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}better_payment {$where_sql}"; 58 51 59 if (!empty($search)) { 52 $search_like = '%' . $wpdb->esc_like($search) . '%'; 53 $where_parts = []; 54 foreach ($columns as $column) { 55 // Prepare each part individually to avoid spread operator and keep SQL literal-ish 56 $where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like); 57 } 58 $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')'; 60 $filtered_records = $this->wpdb->get_var( 61 $this->wpdb->prepare($filtered_records_sql, ...$search_params_final) 62 ); 63 } else { 64 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 59 65 } 60 61 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 62 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 63 64 $order_sql = " ORDER BY $order_column $order_direction"; 65 $data = $wpdb->get_results( 66 $wpdb->prepare( 67 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 68 $start, 69 $length 70 ), 71 ARRAY_A 72 ); 73 // phpcs:enable 74 66 67 // SQL for ordering 68 $order_sql = "ORDER BY {$order_column} {$order_direction}"; 69 70 // Get data with search, order, and pagination 71 $data_sql = "SELECT * FROM {$this->wpdb->prefix}better_payment {$where_sql} {$order_sql} LIMIT %d, %d"; 72 73 if (!empty($search)) { 74 $query_params = array_merge($search_params_final, [$start, $length]); 75 $data = $this->wpdb->get_results( 76 $this->wpdb->prepare($data_sql, ...$query_params), 77 ARRAY_A 78 ); 79 } else { 80 $data = $this->wpdb->get_results( 81 $this->wpdb->prepare($data_sql, $start, $length), 82 ARRAY_A 83 ); 84 } 85 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 86 75 87 // Wrap the option_value in the scrollable-cell div 76 88 foreach ($data as &$row) { -
nhrrob-options-table-manager/trunk/includes/Managers/CommonTableManager.php
r3442179 r3450271 51 51 $total_records = $wpdb->get_var("SELECT COUNT(*) FROM $table"); 52 52 53 $where_sql = ''; 53 // Build WHERE clause for search conditions 54 $where_clauses = []; 55 56 // Global search 54 57 if (!empty($search)) { 55 $search_like = '%' . $wpdb->esc_like($search) . '%'; 56 $where_parts = []; 58 $search_like = '%' . $this->wpdb->esc_like($search) . '%'; 59 $search_params = []; 60 $search_sql_parts = []; 61 62 // Add search for each column 57 63 foreach ($columns as $column) { 58 // Prepare each part individually to avoid spread operator and keep SQL literal-ish59 $ where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like);64 $search_sql_parts[] = "{$column} LIKE %s"; 65 $search_params[] = $search_like; 60 66 } 61 $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')'; 67 68 $where_clauses[] = "(" . implode(' OR ', $search_sql_parts) . ")"; 69 $search_params_final = $search_params; 62 70 } 63 64 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 65 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 66 67 $order_sql = " ORDER BY $order_column $order_direction"; 68 $data = $wpdb->get_results( 69 $wpdb->prepare( 70 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 71 $start, 72 $length 73 ), 74 ARRAY_A 75 ); 76 // phpcs:enable 77 71 72 // Build WHERE clause parts 73 $where_parts = []; 74 if (!empty($search)) { 75 $search_like = '%' . $this->wpdb->esc_like($search) . '%'; 76 $search_sql_parts = []; 77 foreach ($columns as $column) { 78 $search_sql_parts[] = "{$column} LIKE %s"; 79 } 80 $where_parts[] = "(" . implode(' OR ', $search_sql_parts) . ")"; 81 } 82 83 // Count filtered records 84 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 85 if (!empty($search)) { 86 $where_clause = 'WHERE ' . implode(' AND ', $where_parts); 87 $filtered_records = $this->wpdb->get_var( 88 $this->wpdb->prepare( 89 "SELECT COUNT(*) FROM {$this->table_name} $where_clause", 90 ...$search_params_final 91 ) 92 ); 93 } else { 94 $filtered_records = $this->wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"); 95 } 96 97 // Get data with search, order, and pagination 98 if (!empty($search)) { 99 $where_clause = 'WHERE ' . implode(' AND ', $where_parts); 100 $query_params = array_merge($search_params_final, [$start, $length]); 101 $data = $this->wpdb->get_results( 102 $this->wpdb->prepare( 103 "SELECT * FROM {$this->table_name} $where_clause ORDER BY {$order_column} {$order_direction} LIMIT %d, %d", 104 ...$query_params 105 ), 106 ARRAY_A 107 ); 108 } else { 109 $data = $this->wpdb->get_results( 110 $this->wpdb->prepare( 111 "SELECT * FROM {$this->table_name} ORDER BY {$order_column} {$order_direction} LIMIT %d, %d", 112 $start, 113 $length 114 ), 115 ARRAY_A 116 ); 117 } 118 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 119 78 120 // Wrap the option_value in the scrollable-cell div 79 121 foreach ($data as &$row) { -
nhrrob-options-table-manager/trunk/includes/Managers/HistoryManager.php
r3442179 r3450271 71 71 } 72 72 73 // phpcs: disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared73 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific history tracking 74 74 return $wpdb->insert( 75 75 $table, … … 95 95 { 96 96 global $wpdb; 97 $table = $this->table_name; 98 99 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 97 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 100 98 return $wpdb->get_results( 101 99 $wpdb->prepare( 102 "SELECT * FROM $tableWHERE option_name = %s ORDER BY performed_at DESC",100 "SELECT * FROM {$wpdb->prefix}nhrotm_option_history WHERE option_name = %s ORDER BY performed_at DESC", 103 101 sanitize_text_field(wp_unslash($option_name)) 104 102 ), … … 141 139 $table = $this->table_name; 142 140 143 // phpcs: disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared141 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 144 142 $record = $wpdb->get_row( 145 $wpdb->prepare( 146 "SELECT * FROM $table WHERE id = %d", 147 $history_id 148 ), 143 $wpdb->prepare("SELECT * FROM {$wpdb->prefix}nhrotm_option_history WHERE id = %d", $history_id), 149 144 ARRAY_A 150 145 ); … … 199 194 return 'Failed to restore option'; 200 195 } 196 197 /** 198 * Prune history logs older than X days 199 * 200 * @param int $days Number of days to retain 201 * @return int|false Number of rows deleted or false on error 202 */ 203 public function prune_history($days = 30) 204 { 205 global $wpdb; 206 $days = intval($days); 207 if ($days < 1) $days = 30; 208 209 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific deletion 210 return $wpdb->query( 211 $wpdb->prepare( 212 "DELETE FROM {$wpdb->prefix}nhrotm_option_history WHERE performed_at < DATE_SUB(NOW(), INTERVAL %d DAY)", 213 $days 214 ) 215 ); 216 } 201 217 } -
nhrrob-options-table-manager/trunk/includes/Managers/OptimizationManager.php
r3442179 r3450271 58 58 $table = $this->table_name; 59 59 60 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter61 $results = $wpdb->get_results(62 $wpdb->prepare(63 "SELECT option_name, option_value, autoload, LENGTH(option_value) as size_bytes64 FROM $table65 WHERE autoload NOT IN ('off', 'no', 'false', '0', '')66 ORDER BY size_bytes DESC67 LIMIT %d",68 $limit69 ),70 ARRAY_A71 );72 // phpcs:enable60 // Query to get autoloaded options 61 // Broaden the search to catch 'yes', 'true', '1', 'on' by excluding known 'no' values 62 global $wpdb; 63 64 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 65 $results = $wpdb->get_results($wpdb->prepare( 66 "SELECT option_name, option_value, autoload, LENGTH(option_value) as size_bytes 67 FROM {$wpdb->options} 68 WHERE autoload NOT IN ('off', 'no', 'false', '0', '') 69 ORDER BY size_bytes DESC 70 LIMIT %d", 71 $limit 72 ), ARRAY_A); 73 73 74 74 return array_map(function ($row) { … … 136 136 { 137 137 global $wpdb; 138 $table = $this->table_name; 139 140 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 141 $bytes = $wpdb->get_var( 142 "SELECT SUM(LENGTH(option_value)) FROM $table WHERE autoload NOT IN ('no', 'false', '0', '')" 143 ); 144 // phpcs:enable 138 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query 139 $bytes = $wpdb->get_var("SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options} WHERE autoload NOT IN ('no', 'false', '0', '')"); 145 140 return size_format($bytes ? $bytes : 0); 146 141 } -
nhrrob-options-table-manager/trunk/includes/Managers/OptionsTableManager.php
r3442179 r3450271 141 141 142 142 // Count filtered records 143 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 144 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 143 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 144 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}options {$where_sql}"; 145 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 145 146 146 147 // SQL for ordering … … 148 149 149 150 // Get data with search, order, and pagination 150 $data = $wpdb->get_results( 151 $wpdb->prepare( 152 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 153 $start, 154 $length 155 ), 151 $data_sql = "SELECT * FROM {$this->wpdb->prefix}options {$where_sql} {$order_sql} LIMIT %d, %d"; 152 $data = $this->wpdb->get_results( 153 $this->wpdb->prepare($data_sql, $start, $length), 156 154 ARRAY_A 157 155 ); 158 // phpcs:enable 156 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 159 157 160 158 // Wrap the option_value in the scrollable-cell div … … 386 384 $this->validate_permissions(); 387 385 388 $option_names = isset($_POST['option_names']) ? array_map('sanitize_text_field', (array) wp_unslash($_POST['option_names'])) : []; 386 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in array_map below 387 $option_names = isset($_POST['option_names']) ? wp_unslash((array) $_POST['option_names']) : []; 388 $option_names = array_map('sanitize_text_field', $option_names); 389 389 390 390 if (empty($option_names)) { -
nhrrob-options-table-manager/trunk/includes/Managers/UsermetaTableManager.php
r3442179 r3450271 54 54 $where_sql = $wpdb->prepare(" WHERE (meta_key LIKE %s OR meta_value LIKE %s)", $search_like, $search_like); 55 55 } 56 57 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 58 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 59 60 $order_sql = " ORDER BY $order_column $order_direction"; 61 $data = $wpdb->get_results( 62 $wpdb->prepare( 63 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 64 $start, 65 $length 66 ), 56 57 // Count filtered records 58 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 59 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}usermeta {$where_sql}"; 60 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 61 62 // SQL for ordering 63 $order_sql = "ORDER BY {$order_column} {$order_direction}"; 64 65 // Get data with search, order, and pagination 66 $data_sql = "SELECT * FROM {$this->wpdb->prefix}usermeta {$where_sql} {$order_sql} LIMIT %d, %d"; 67 $data = $this->wpdb->get_results( 68 $this->wpdb->prepare($data_sql, $start, $length), 67 69 ARRAY_A 68 70 ); 69 // phpcs:enable 70 71 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 72 71 73 // Wrap the option_value in the scrollable-cell div 72 74 foreach ($data as &$row) { -
nhrrob-options-table-manager/trunk/includes/Managers/WprmRatingsTableManager.php
r3442179 r3450271 49 49 50 50 $where_sql = ''; 51 if (!empty($where_clauses)) { 52 $where_sql = 'WHERE ' . implode(' AND ', $where_clauses); 53 } 54 55 // Count filtered records 56 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 57 $filtered_records_sql = "SELECT COUNT(*) FROM {$this->wpdb->prefix}wprm_ratings {$where_sql}"; 58 51 59 if (!empty($search)) { 52 $search_like = '%' . $wpdb->esc_like($search) . '%'; 53 $where_parts = []; 54 foreach ($columns as $column) { 55 // Prepare each part individually to avoid spread operator and keep SQL literal-ish 56 $where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like); 57 } 58 $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')'; 60 $filtered_records = $this->wpdb->get_var( 61 $this->wpdb->prepare($filtered_records_sql, ...$search_params_final) 62 ); 63 } else { 64 $filtered_records = $this->wpdb->get_var($filtered_records_sql); 59 65 } 60 61 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter 62 $filtered_records = $wpdb->get_var("SELECT COUNT(*) FROM $table $where_sql"); 63 64 $order_sql = " ORDER BY $order_column $order_direction"; 65 $data = $wpdb->get_results( 66 $wpdb->prepare( 67 "SELECT * FROM $table $where_sql $order_sql LIMIT %d, %d", 68 $start, 69 $length 70 ), 71 ARRAY_A 72 ); 73 // phpcs:enable 74 66 67 // SQL for ordering 68 $order_sql = "ORDER BY {$order_column} {$order_direction}"; 69 70 // Get data with search, order, and pagination 71 $data_sql = "SELECT * FROM {$this->wpdb->prefix}wprm_ratings {$where_sql} {$order_sql} LIMIT %d, %d"; 72 73 if (!empty($search)) { 74 $query_params = array_merge($search_params_final, [$start, $length]); 75 $data = $this->wpdb->get_results( 76 $this->wpdb->prepare($data_sql, ...$query_params), 77 ARRAY_A 78 ); 79 } else { 80 $data = $this->wpdb->get_results( 81 $this->wpdb->prepare($data_sql, $start, $length), 82 ARRAY_A 83 ); 84 } 85 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter 86 75 87 // Wrap the option_value in the scrollable-cell div 76 88 foreach ($data as &$row) { -
nhrrob-options-table-manager/trunk/includes/views/admin/settings/index.php
r3442179 r3450271 44 44 <button class="tablinks settings-tab" data-tab="nhrotm-settings-tab"> 45 45 <?php esc_html_e('Settings', 'nhrrob-options-table-manager'); ?> 46 </button> 47 <button class="tablinks scanner-tab" data-tab="nhrotm-scanner-tab"> 48 <?php esc_html_e('Orphan Scanner', 'nhrrob-options-table-manager'); ?> 49 </button> 50 <button class="tablinks search-replace-tab" data-tab="nhrotm-search-replace-tab"> 51 <?php esc_html_e('Search & Replace', 'nhrrob-options-table-manager'); ?> 52 </button> 53 <button class="tablinks import-export-tab" data-tab="nhrotm-import-export-tab"> 54 <?php esc_html_e('Import / Export', 'nhrrob-options-table-manager'); ?> 46 55 </button> 47 56 </div> … … 110 119 </div> 111 120 112 <div id="nhrotm-usermeta-tab" class="nhrotm-tab-content " style="display:none;">121 <div id="nhrotm-usermeta-tab" class="nhrotm-tab-content d-none"> 113 122 <table id="nhrotm-data-table-usermeta" class="nhrotm-data-table wp-list-table widefat fixed striped"> 114 123 <thead> … … 126 135 127 136 <?php if ($is_better_payment_installed): ?> 128 <div id="nhrotm-better-payment-tab" class="nhrotm-tab-content " style="display:none;">137 <div id="nhrotm-better-payment-tab" class="nhrotm-tab-content d-none"> 129 138 <table id="nhrotm-data-table-better_payment" class="nhrotm-data-table wp-list-table widefat fixed striped"> 130 139 <thead> … … 146 155 147 156 <?php if ($is_wp_recipe_maker_installed): ?> 148 <div id="nhrotm-wprm-ratings-tab" class="nhrotm-tab-content " style="display:none;">157 <div id="nhrotm-wprm-ratings-tab" class="nhrotm-tab-content d-none"> 149 158 <table id="nhrotm-data-table-wprm_ratings" class="nhrotm-data-table wp-list-table widefat fixed striped"> 150 159 <thead> … … 166 175 </div> 167 176 168 <div id="nhrotm-wprm-analytics-tab" class="nhrotm-tab-content " style="display:none;">177 <div id="nhrotm-wprm-analytics-tab" class="nhrotm-tab-content d-none"> 169 178 <table id="nhrotm-data-table-wprm_analytics" class="nhrotm-data-table wp-list-table widefat fixed striped"> 170 179 <thead> … … 185 194 </div> 186 195 187 <div id="nhrotm-wprm-changelog-tab" class="nhrotm-tab-content " style="display:none;">196 <div id="nhrotm-wprm-changelog-tab" class="nhrotm-tab-content d-none"> 188 197 <table id="nhrotm-data-table-wprm_changelog" class="nhrotm-data-table wp-list-table widefat fixed striped"> 189 198 <thead> … … 300 309 <!-- History Modal --> 301 310 <div class="nhrotm-history-modal is-hidden"> 302 <div class="nhrotm-modal-content " style="max-width: 800px; width: 90%;">311 <div class="nhrotm-modal-content nhrotm-modal-lg"> 303 312 <h2><?php esc_html_e('Option History', 'nhrrob-options-table-manager'); ?>: <span 304 313 class="nhrotm-history-option-name"></span></h2> 305 314 306 <div class="nhrotm-history-loading " style="display:none; text-align: center; padding: 20px;">315 <div class="nhrotm-history-loading nhrotm-loader-box d-none"> 307 316 Loading... 308 317 </div> 309 318 310 <div class="nhrotm-history-list-container " style="max-height: 500px; overflow-y: auto;">311 <table class="wp-list-table widefat fixed striped " style="width:100%">319 <div class="nhrotm-history-list-container nhrotm-scrollable-vh-50"> 320 <table class="wp-list-table widefat fixed striped w-full"> 312 321 <thead> 313 322 <tr> … … 325 334 </div> 326 335 327 <button class="button nhrotm-close-history-modal " style="margin-top: 15px;">Close</button>336 <button class="button nhrotm-close-history-modal mt-3">Close</button> 328 337 </div> 329 338 </div> 330 339 331 <div id="nhrotm-autoload-optimizer-tab" class="nhrotm-tab-content " style="display:none;">332 <div class="card " style="max-width: 100%; margin-top: 20px; padding: 20px;">340 <div id="nhrotm-autoload-optimizer-tab" class="nhrotm-tab-content d-none"> 341 <div class="card nhrotm-card-full"> 333 342 <h2>Autoload Health Check</h2> 334 343 <div class="nhrotm-autoload-stats"> … … 352 361 </tbody> 353 362 </table> 354 </div> 355 </div> 356 357 <div id="nhrotm-settings-tab" class="nhrotm-tab-content" style="display:none;"> 358 <div class="card" style="max-width: 100%; margin-top: 20px; padding: 20px;"> 363 364 <div class="nhrotm-history-retention-settings mt-5 nhrotm-section-divider"> 365 <h3><?php esc_html_e('History Retention', 'nhrrob-options-table-manager'); ?></h3> 366 <p><?php esc_html_e('To prevent the history log from growing too large, you can automatically delete old logs.', 'nhrrob-options-table-manager'); ?></p> 367 <form id="nhrotm-history-settings-form"> 368 <table class="form-table"> 369 <tr> 370 <th scope="row"><label for="nhrotm_history_retention_days"><?php esc_html_e('Keep logs for (days)', 'nhrrob-options-table-manager'); ?></label></th> 371 <td> 372 <input name="nhrotm_history_retention_days" type="number" id="nhrotm_history_retention_days" value="<?php echo esc_attr(get_option('nhrotm_history_retention_days', 30)); ?>" class="small-text" min="1"> 373 <p class="description"><?php esc_html_e('Logs older than this will be automatically deleted daily.', 'nhrrob-options-table-manager'); ?></p> 374 </td> 375 </tr> 376 </table> 377 <p class="submit"> 378 <input type="submit" name="submit" id="nhrotm-save-history-settings" class="button button-primary" value="<?php esc_attr_e('Save Changes', 'nhrrob-options-table-manager'); ?>"> 379 <button type="button" id="nhrotm-prune-history-now" class="button button-secondary"><?php esc_html_e('Prune Now', 'nhrrob-options-table-manager'); ?></button> 380 </p> 381 </form> 382 </div> 383 </div> 384 </div> 385 386 <div id="nhrotm-scanner-tab" class="nhrotm-tab-content d-none"> 387 <div class="card nhrotm-tab-card"> 388 <h2>Orphaned Options Scanner</h2> 389 <p class="description">Scan your database for options left behind by uninstalled or inactive plugins. Identifying these helps reduce database bloat.</p> 390 391 <div class="nhrotm-scanner-actions mb-4"> 392 <button id="nhrotm-start-scan" class="button button-primary">Start Deep Scan</button> 393 </div> 394 395 <div class="nhrotm-scanner-loading loader-container d-none"> 396 <span class="spinner is-active nhrotm-spinner-centered"></span> 397 <p>Analyzing database prefixes and cross-referencing with plugin directories...</p> 398 </div> 399 400 <div id="nhrotm-scanner-results" class="d-none"> 401 <table class="wp-list-table widefat fixed striped"> 402 <thead> 403 <tr> 404 <th>Prefix Candidate</th> 405 <th>Option Count</th> 406 <th>Likely Source</th> 407 <th>Risk Level</th> 408 <th>Action</th> 409 </tr> 410 </thead> 411 <tbody id="nhrotm-scanner-list-body"> 412 <!-- Scan results --> 413 </tbody> 414 </table> 415 </div> 416 417 <div id="nhrotm-scanner-empty" class="loader-container d-none"> 418 <p>No significant orphaned options detected. Your database looks clean!</p> 419 </div> 420 </div> 421 </div> 422 423 <div id="nhrotm-search-replace-tab" class="nhrotm-tab-content d-none"> 424 <div class="card nhrotm-tab-card"> 425 <h2>Global Search & Replace</h2> 426 <p class="description">Find and replace strings across your entire options table. Supports serialized data and JSON safely.</p> 427 428 <div class="nhrotm-search-replace-form mb-4 max-w-600"> 429 <div class="nhrotm-form-field mb-3"> 430 <label class="font-semibold d-block mb-1">Search for:</label> 431 <input type="text" id="nhrotm-search-string" class="regular-text" placeholder="e.g. old-domain.com"> 432 </div> 433 <div class="nhrotm-form-field mb-3"> 434 <label class="font-semibold d-block mb-1">Replace with:</label> 435 <input type="text" id="nhrotm-replace-string" class="regular-text" placeholder="e.g. new-domain.com"> 436 </div> 437 <div class="nhrotm-form-field mb-4"> 438 <label class="nhrotm-switch-label d-flex items-center gap-10 pointer"> 439 <input type="checkbox" id="nhrotm-dry-run-toggle" checked> 440 <span>Dry Run (Preview changes without saving)</span> 441 </label> 442 </div> 443 444 <div class="nhrotm-form-actions"> 445 <button type="button" id="nhrotm-search-replace-btn" class="button button-primary">Execute Replacement</button> 446 </div> 447 </div> 448 449 <div class="nhrotm-search-replace-loading loader-container d-none"> 450 <span class="spinner is-active nhrotm-spinner-centered"></span> 451 <p>Processing database records... This may take a moment.</p> 452 </div> 453 454 <div id="nhrotm-search-replace-results" class="mt-5 d-none"> 455 <div class="nhrotm-sr-summary nhrotm-summary-box"> 456 <p id="nhrotm-sr-summary-text" class="m-0 font-semibold"></p> 457 </div> 458 459 <table class="wp-list-table widefat fixed striped"> 460 <thead> 461 <tr> 462 <th>Option Name</th> 463 <th>Occurrences Found</th> 464 </tr> 465 </thead> 466 <tbody id="nhrotm-sr-list-body"> 467 <!-- Results --> 468 </tbody> 469 </table> 470 </div> 471 </div> 472 </div> 473 474 <div id="nhrotm-import-export-tab" class="nhrotm-tab-content d-none"> 475 <div class="card nhrotm-tab-card"> 476 <h2>Import / Export Options</h2> 477 <p class="description">Selectively export options or import a configuration file from another site.</p> 478 479 <div class="nhrotm-ie-columns d-flex gap-40 mt-30"> 480 <!-- Export Section --> 481 <div class="nhrotm-ie-column flex-1"> 482 <h3>Export Options</h3> 483 <p>Search and select options to add to your export basket.</p> 484 485 <div class="nhrotm-form-field mb-3"> 486 <input type="text" id="nhrotm-export-search" class="regular-text w-full" placeholder="Search option names..."> 487 <div id="nhrotm-export-suggestions" class="nhrotm-suggestions-box d-none"></div> 488 </div> 489 490 <div class="nhrotm-export-basket"> 491 <h4>Selected Options (<span id="nhrotm-basket-count">0</span>)</h4> 492 <ul id="nhrotm-basket-list"> 493 <li class="empty-basket">No options selected.</li> 494 </ul> 495 </div> 496 497 <button id="nhrotm-do-export" class="button button-primary mt-3" disabled>Export to JSON</button> 498 </div> 499 500 <!-- Import Section --> 501 <div class="nhrotm-ie-column flex-1 nhrotm-ie-border-left"> 502 <h3>Import Options</h3> 503 <p>Upload a previously exported JSON file.</p> 504 505 <div class="nhrotm-form-field mb-3"> 506 <input type="file" id="nhrotm-import-file" accept=".json"> 507 </div> 508 509 <div id="nhrotm-import-preview-area" class="d-none"> 510 <h4>Import Preview</h4> 511 <div class="nhrotm-import-stats mb-2"> 512 Found <span id="nhrotm-import-total">0</span> options. 513 </div> 514 515 <div class="nhrotm-scrollable-table nhrotm-scrollable-vh-30"> 516 <table class="wp-list-table widefat fixed striped"> 517 <thead> 518 <tr> 519 <td class="check-column"><input type="checkbox" id="nhrotm-import-select-all" checked></td> 520 <th>Option Name</th> 521 <th>Status</th> 522 <th>Current Val (Snippet)</th> 523 </tr> 524 </thead> 525 <tbody id="nhrotm-import-preview-body"></tbody> 526 </table> 527 </div> 528 529 <button id="nhrotm-execute-import" class="button button-primary mt-3">Execute Import</button> 530 </div> 531 </div> 532 </div> 533 534 </div> 535 </div> 536 537 <div id="nhrotm-settings-tab" class="nhrotm-tab-content d-none"> 538 <div class="card nhrotm-tab-card"> 359 539 <h2>Settings</h2> 360 540 <table class="form-table"> -
nhrrob-options-table-manager/trunk/nhrrob-options-table-manager.php
r3442179 r3450271 6 6 * Author: Nazmul Hasan Robin 7 7 * Author URI: https://profiles.wordpress.org/nhrrob/ 8 * Version: 1. 2.08 * Version: 1.3.0 9 9 * Requires at least: 6.0 10 10 * Requires PHP: 7.4 … … 30 30 * @var string 31 31 */ 32 const nhrotm_version = '1. 1.9';32 const nhrotm_version = '1.3.0'; 33 33 34 34 /** … … 55 55 wp_schedule_event(time(), 'daily', 'nhrotm_daily_cleanup'); 56 56 } 57 58 if (!wp_next_scheduled('nhrotm_daily_history_prune')) { 59 wp_schedule_event(time(), 'daily', 'nhrotm_daily_history_prune'); 60 } 57 61 } 58 62 … … 63 67 { 64 68 wp_clear_scheduled_hook('nhrotm_daily_cleanup'); 69 wp_clear_scheduled_hook('nhrotm_daily_history_prune'); 65 70 } 66 71 … … 107 112 // Cron Handler 108 113 add_action('nhrotm_daily_cleanup', [$this, 'run_cleanup']); 114 add_action('nhrotm_daily_history_prune', [$this, 'run_history_prune']); 109 115 110 116 new Nhrotm\OptionsTableManager\Assets(); … … 112 118 if (defined('DOING_AJAX') && DOING_AJAX) { 113 119 new Nhrotm\OptionsTableManager\Ajax\AjaxHandler(); 120 } 121 122 if (defined('WP_CLI') && WP_CLI) { 123 \WP_CLI::add_command('nhr-options', '\Nhrotm\OptionsTableManager\Cli\CliCommands'); 114 124 } 115 125 … … 130 140 } 131 141 } 142 143 /** 144 * Run history pruning 145 */ 146 public function run_history_prune() 147 { 148 $days = get_option('nhrotm_history_retention_days', 30); 149 $history_manager = new \Nhrotm\OptionsTableManager\Managers\HistoryManager(); 150 $history_manager->prune_history($days); 151 } 132 152 } 133 153 -
nhrrob-options-table-manager/trunk/readme.txt
r3442179 r3450271 1 1 === NHR Advanced Options Table Manager & Autoload Optimizer === 2 2 Contributors: nhrrob 3 Tags: wp_options, transients, usermeta, autoload-optimizer, database-optimization3 Tags: wp_options, transients, usermeta, optimize, database-optimization 4 4 Requires at least: 6.0 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.07 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 37 37 - **Live Search & Pagination** – High-performance DataTables with server-side processing. 38 38 - **Security & Optimization** – Protection for core WordPress options to prevent accidental data loss. 39 - **Import / Export** – Move settings between sites easily with JSON support. 40 - **Global Search & Replace** – Safely replace strings across the database with dry-run preview. 41 - **Orphan Scanner** – Find and clean up leftovers from uninstalled plugins. 42 - **WP-CLI Support** – Manage options (wp nhr-options list, wp nhr-options delete) from the command line. 39 43 40 44 ### 🚀 Coming Soon 41 45 We're constantly improving NHR Options Table Manager! Here's what's on the way: 42 - **Export Data** – Export your options and analytics to CSV or JSON formats.43 46 - **Scheduled Backups** – Automatically backup your `wp_options` table before major changes. 44 47 … … 74 77 75 78 **Can I delete expired transients?** 76 Not yet, but this feature is coming soon! 79 Yes! We have an automated daily cleanup feature and a manual delete button. 77 80 78 81 == Screenshots == … … 86 89 87 90 == Changelog == 91 92 = 1.3.0 - 30/01/2026 = 93 - Added: Export/Import feature allowing JSON configuration portability 94 - Added: Global Search & Replace utility with safe serialization handling and Dry Run mode 95 - Added: Orphaned Options Scanner to identifying bloat from uninstalled plugins 96 - Added: Option History Pruning (Automated via Cron & Manual control) 97 - Added: WP-CLI Support (`nhr-options list`, `nhr-options delete`) 88 98 89 99 = 1.2.0 - 19/01/2026 =
Note: See TracChangeset
for help on using the changeset viewer.