Plugin Directory

Changeset 3450271


Ignore:
Timestamp:
01/30/2026 09:25:24 AM (8 weeks ago)
Author:
nhrrob
Message:

Update to version 1.3.0 from GitHub

Location:
nhrrob-options-table-manager
Files:
10 added
26 edited
1 copied

Legend:

Unmodified
Added
Removed
  • nhrrob-options-table-manager/tags/1.3.0/assets/css/admin.css

    r3442179 r3450271  
    99}
    1010
     11.d-block {
     12    display: block;
     13}
     14
    1115.m-auto {
    1216    margin: auto;
    1317}
    1418
     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
    1564.m-1 {
    1665    margin: 5px;
     
    53102}
    54103
     104.mt-30 {
     105    margin-top: 30px;
     106}
     107
    55108.mb-1 {
    56109    margin-bottom: 5px;
     
    215268.text-center {
    216269    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;
    217305}
    218306
     
    231319}
    232320
     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
    233331/* Modal design  */
    234332.nhrotm-add-option-modal,
     
    237335.nhrotm-history-modal {
    238336    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 */
    241341    left: 0;
    242342    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 */
    247351}
    248352
    249353.nhrotm-modal-content {
    250354    background-color: #fefefe;
    251     margin: 15% auto; /* 15% from the top and centered */
     355    margin: 15% auto;
     356    /* 15% from the top and centered */
    252357    padding: 20px;
    253358    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%;
    257370}
    258371
     
    524637    border-left: 4px solid #2271b1;
    525638}
     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  
    849849
    850850            // 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           
    854862            if (isFeatureTab) {
    855863                $('.nhrotm-filter-container').hide();
    856                 $('.nhrotm-add-option-button').hide();
    857864                $('.logged-user-id').hide();
    858865            } else {
     
    860867                $('.logged-user-id').show();
    861868
    862                 // "Add Option" is specifically for the main Options Table
    863                 if ($(this).hasClass('options-table')) {
    864                     $('.nhrotm-add-option-button').show();
    865                 } else {
    866                     $('.nhrotm-add-option-button').hide();
    867                 }
    868 
    869869                // Adjust DataTables inside this tab automatically
    870870                $targetContainer.find('table.nhrotm-data-table').each(function () {
     
    878878            if ($(this).hasClass('optimization-tab')) {
    879879                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();
    880886            }
    881887        });
     
    10141020        });
    10151021
     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
    10161452    });
    10171453})(jQuery);
  • nhrrob-options-table-manager/tags/1.3.0/includes/Ajax/AjaxHandler.php

    r3442179 r3450271  
    1212use Nhrotm\OptionsTableManager\Managers\WprmRatingsTableManager;
    1313use Nhrotm\OptionsTableManager\Managers\OptimizationManager;
     14use Nhrotm\OptionsTableManager\Managers\ScannerManager;
     15use Nhrotm\OptionsTableManager\Managers\SearchReplaceManager;
     16use Nhrotm\OptionsTableManager\Managers\ImportExportManager;
    1417
    1518class AjaxHandler
     
    2023    private $wprm_ratings_manager;
    2124    private $optimization_manager;
     25    private $scanner_manager;
     26    private $search_replace_manager;
     27    private $import_export_manager;
    2228    protected $wpdb;
    2329
     
    2935        $this->wprm_ratings_manager = new WprmRatingsTableManager();
    3036        $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();
    3140
    3241        global $wpdb;
     
    6574            // Auto Cleanup
    6675            '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',
    6791        ];
    6892
     
    293317
    294318        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            }
    295322            $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 20;
    296323            $data = $this->optimization_manager->get_heavy_autoload_options($limit);
     
    312339
    313340        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            }
    314344            $result = $this->optimization_manager->toggle_autoload();
    315345            if ($result) {
     
    356386        wp_send_json_success('Settings updated');
    357387    }
     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    }
    358607}
  • nhrrob-options-table-manager/tags/1.3.0/includes/Managers/BetterPaymentTableManager.php

    r3442179 r3450271  
    4949
    5050        $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       
    5159        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);
    5965        }
    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       
    7587        // Wrap the option_value in the scrollable-cell div
    7688        foreach ($data as &$row) {
  • nhrrob-options-table-manager/tags/1.3.0/includes/Managers/CommonTableManager.php

    r3442179 r3450271  
    5151        $total_records = $wpdb->get_var("SELECT COUNT(*) FROM $table");
    5252
    53         $where_sql = '';
     53        // Build WHERE clause for search conditions
     54        $where_clauses = [];
     55       
     56        // Global search
    5457        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
    5763            foreach ($columns as $column) {
    58                 // Prepare each part individually to avoid spread operator and keep SQL literal-ish
    59                 $where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like);
     64                $search_sql_parts[] = "{$column} LIKE %s";
     65                $search_params[] = $search_like;
    6066            }
    61             $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')';
     67           
     68            $where_clauses[] = "(" . implode(' OR ', $search_sql_parts) . ")";
     69            $search_params_final = $search_params;
    6270        }
    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       
    78120        // Wrap the option_value in the scrollable-cell div
    79121        foreach ($data as &$row) {
  • nhrrob-options-table-manager/tags/1.3.0/includes/Managers/HistoryManager.php

    r3442179 r3450271  
    7171        }
    7272
    73         // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     73        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific history tracking
    7474        return $wpdb->insert(
    7575            $table,
     
    9595    {
    9696        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
    10098        return $wpdb->get_results(
    10199            $wpdb->prepare(
    102                 "SELECT * FROM $table WHERE 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",
    103101                sanitize_text_field(wp_unslash($option_name))
    104102            ),
     
    141139        $table = $this->table_name;
    142140
    143         // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     141        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query
    144142        $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),
    149144            ARRAY_A
    150145        );
     
    199194        return 'Failed to restore option';
    200195    }
     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    }
    201217}
  • nhrrob-options-table-manager/tags/1.3.0/includes/Managers/OptimizationManager.php

    r3442179 r3450271  
    5858        $table = $this->table_name;
    5959
    60         // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    61         $results = $wpdb->get_results(
    62             $wpdb->prepare(
    63                 "SELECT option_name, option_value, autoload, LENGTH(option_value) as size_bytes
    64                 FROM $table
    65                 WHERE autoload NOT IN ('off', 'no', 'false', '0', '')
    66                 ORDER BY size_bytes DESC
    67                 LIMIT %d",
    68                 $limit
    69             ),
    70             ARRAY_A
    71         );
    72         // phpcs:enable
     60        // 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);
    7373
    7474        return array_map(function ($row) {
     
    136136    {
    137137        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', '')");
    145140        return size_format($bytes ? $bytes : 0);
    146141    }
  • nhrrob-options-table-manager/tags/1.3.0/includes/Managers/OptionsTableManager.php

    r3442179 r3450271  
    141141
    142142        // 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);
    145146
    146147        // SQL for ordering
     
    148149
    149150        // 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),
    156154            ARRAY_A
    157155        );
    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
    159157
    160158        // Wrap the option_value in the scrollable-cell div
     
    386384        $this->validate_permissions();
    387385
    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);
    389389
    390390        if (empty($option_names)) {
  • nhrrob-options-table-manager/tags/1.3.0/includes/Managers/UsermetaTableManager.php

    r3442179 r3450271  
    5454            $where_sql = $wpdb->prepare(" WHERE (meta_key LIKE %s OR meta_value LIKE %s)", $search_like, $search_like);
    5555        }
    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),
    6769            ARRAY_A
    6870        );
    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       
    7173        // Wrap the option_value in the scrollable-cell div
    7274        foreach ($data as &$row) {
  • nhrrob-options-table-manager/tags/1.3.0/includes/Managers/WprmRatingsTableManager.php

    r3442179 r3450271  
    4949
    5050        $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       
    5159        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);
    5965        }
    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       
    7587        // Wrap the option_value in the scrollable-cell div
    7688        foreach ($data as &$row) {
  • nhrrob-options-table-manager/tags/1.3.0/includes/views/admin/settings/index.php

    r3442179 r3450271  
    4444            <button class="tablinks settings-tab" data-tab="nhrotm-settings-tab">
    4545                <?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'); ?>
    4655            </button>
    4756        </div>
     
    110119        </div>
    111120
    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">
    113122            <table id="nhrotm-data-table-usermeta" class="nhrotm-data-table wp-list-table widefat fixed striped">
    114123                <thead>
     
    126135
    127136        <?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">
    129138                <table id="nhrotm-data-table-better_payment" class="nhrotm-data-table wp-list-table widefat fixed striped">
    130139                    <thead>
     
    146155
    147156        <?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">
    149158                <table id="nhrotm-data-table-wprm_ratings" class="nhrotm-data-table wp-list-table widefat fixed striped">
    150159                    <thead>
     
    166175            </div>
    167176
    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">
    169178                <table id="nhrotm-data-table-wprm_analytics" class="nhrotm-data-table wp-list-table widefat fixed striped">
    170179                    <thead>
     
    185194            </div>
    186195
    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">
    188197                <table id="nhrotm-data-table-wprm_changelog" class="nhrotm-data-table wp-list-table widefat fixed striped">
    189198                    <thead>
     
    300309<!-- History Modal -->
    301310<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">
    303312        <h2><?php esc_html_e('Option History', 'nhrrob-options-table-manager'); ?>: <span
    304313                class="nhrotm-history-option-name"></span></h2>
    305314
    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">
    307316            Loading...
    308317        </div>
    309318
    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">
    312321                <thead>
    313322                    <tr>
     
    325334        </div>
    326335
    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>
    328337    </div>
    329338</div>
    330339
    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">
    333342            <h2>Autoload Health Check</h2>
    334343            <div class="nhrotm-autoload-stats">
     
    352361                </tbody>
    353362            </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">
    359539            <h2>Settings</h2>
    360540            <table class="form-table">
  • nhrrob-options-table-manager/tags/1.3.0/nhrrob-options-table-manager.php

    r3442179 r3450271  
    66 * Author: Nazmul Hasan Robin
    77 * Author URI: https://profiles.wordpress.org/nhrrob/
    8  * Version: 1.2.0
     8 * Version: 1.3.0
    99 * Requires at least: 6.0
    1010 * Requires PHP: 7.4
     
    3030     * @var string
    3131     */
    32     const nhrotm_version = '1.1.9';
     32    const nhrotm_version = '1.3.0';
    3333
    3434    /**
     
    5555            wp_schedule_event(time(), 'daily', 'nhrotm_daily_cleanup');
    5656        }
     57
     58        if (!wp_next_scheduled('nhrotm_daily_history_prune')) {
     59            wp_schedule_event(time(), 'daily', 'nhrotm_daily_history_prune');
     60        }
    5761    }
    5862
     
    6367    {
    6468        wp_clear_scheduled_hook('nhrotm_daily_cleanup');
     69        wp_clear_scheduled_hook('nhrotm_daily_history_prune');
    6570    }
    6671
     
    107112        // Cron Handler
    108113        add_action('nhrotm_daily_cleanup', [$this, 'run_cleanup']);
     114        add_action('nhrotm_daily_history_prune', [$this, 'run_history_prune']);
    109115
    110116        new Nhrotm\OptionsTableManager\Assets();
     
    112118        if (defined('DOING_AJAX') && DOING_AJAX) {
    113119            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');
    114124        }
    115125
     
    130140        }
    131141    }
     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    }
    132152}
    133153
  • nhrrob-options-table-manager/tags/1.3.0/readme.txt

    r3442179 r3450271  
    11=== NHR Advanced Options Table Manager & Autoload Optimizer ===
    22Contributors: nhrrob 
    3 Tags: wp_options, transients, usermeta, autoload-optimizer, database-optimization
     3Tags: wp_options, transients, usermeta, optimize, database-optimization
    44Requires at least: 6.0 
    55Tested up to: 6.9
    66Requires PHP: 7.4 
    7 Stable tag: 1.2.0
     7Stable tag: 1.3.0
    88License: GPLv2 or later 
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html 
     
    3737- **Live Search & Pagination** – High-performance DataTables with server-side processing.
    3838- **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.
    3943
    4044### 🚀 Coming Soon
    4145We'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.
    4346- **Scheduled Backups** – Automatically backup your `wp_options` table before major changes.
    4447
     
    7477
    7578**Can I delete expired transients?** 
    76 Not yet, but this feature is coming soon!
     79Yes! We have an automated daily cleanup feature and a manual delete button.
    7780
    7881== Screenshots ==
     
    8689
    8790== 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`)
    8898
    8999= 1.2.0 - 19/01/2026 =
  • nhrrob-options-table-manager/trunk/assets/css/admin.css

    r3442179 r3450271  
    99}
    1010
     11.d-block {
     12    display: block;
     13}
     14
    1115.m-auto {
    1216    margin: auto;
    1317}
    1418
     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
    1564.m-1 {
    1665    margin: 5px;
     
    53102}
    54103
     104.mt-30 {
     105    margin-top: 30px;
     106}
     107
    55108.mb-1 {
    56109    margin-bottom: 5px;
     
    215268.text-center {
    216269    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;
    217305}
    218306
     
    231319}
    232320
     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
    233331/* Modal design  */
    234332.nhrotm-add-option-modal,
     
    237335.nhrotm-history-modal {
    238336    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 */
    241341    left: 0;
    242342    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 */
    247351}
    248352
    249353.nhrotm-modal-content {
    250354    background-color: #fefefe;
    251     margin: 15% auto; /* 15% from the top and centered */
     355    margin: 15% auto;
     356    /* 15% from the top and centered */
    252357    padding: 20px;
    253358    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%;
    257370}
    258371
     
    524637    border-left: 4px solid #2271b1;
    525638}
     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  
    849849
    850850            // 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           
    854862            if (isFeatureTab) {
    855863                $('.nhrotm-filter-container').hide();
    856                 $('.nhrotm-add-option-button').hide();
    857864                $('.logged-user-id').hide();
    858865            } else {
     
    860867                $('.logged-user-id').show();
    861868
    862                 // "Add Option" is specifically for the main Options Table
    863                 if ($(this).hasClass('options-table')) {
    864                     $('.nhrotm-add-option-button').show();
    865                 } else {
    866                     $('.nhrotm-add-option-button').hide();
    867                 }
    868 
    869869                // Adjust DataTables inside this tab automatically
    870870                $targetContainer.find('table.nhrotm-data-table').each(function () {
     
    878878            if ($(this).hasClass('optimization-tab')) {
    879879                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();
    880886            }
    881887        });
     
    10141020        });
    10151021
     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
    10161452    });
    10171453})(jQuery);
  • nhrrob-options-table-manager/trunk/includes/Ajax/AjaxHandler.php

    r3442179 r3450271  
    1212use Nhrotm\OptionsTableManager\Managers\WprmRatingsTableManager;
    1313use Nhrotm\OptionsTableManager\Managers\OptimizationManager;
     14use Nhrotm\OptionsTableManager\Managers\ScannerManager;
     15use Nhrotm\OptionsTableManager\Managers\SearchReplaceManager;
     16use Nhrotm\OptionsTableManager\Managers\ImportExportManager;
    1417
    1518class AjaxHandler
     
    2023    private $wprm_ratings_manager;
    2124    private $optimization_manager;
     25    private $scanner_manager;
     26    private $search_replace_manager;
     27    private $import_export_manager;
    2228    protected $wpdb;
    2329
     
    2935        $this->wprm_ratings_manager = new WprmRatingsTableManager();
    3036        $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();
    3140
    3241        global $wpdb;
     
    6574            // Auto Cleanup
    6675            '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',
    6791        ];
    6892
     
    293317
    294318        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            }
    295322            $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 20;
    296323            $data = $this->optimization_manager->get_heavy_autoload_options($limit);
     
    312339
    313340        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            }
    314344            $result = $this->optimization_manager->toggle_autoload();
    315345            if ($result) {
     
    356386        wp_send_json_success('Settings updated');
    357387    }
     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    }
    358607}
  • nhrrob-options-table-manager/trunk/includes/Managers/BetterPaymentTableManager.php

    r3442179 r3450271  
    4949
    5050        $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       
    5159        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);
    5965        }
    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       
    7587        // Wrap the option_value in the scrollable-cell div
    7688        foreach ($data as &$row) {
  • nhrrob-options-table-manager/trunk/includes/Managers/CommonTableManager.php

    r3442179 r3450271  
    5151        $total_records = $wpdb->get_var("SELECT COUNT(*) FROM $table");
    5252
    53         $where_sql = '';
     53        // Build WHERE clause for search conditions
     54        $where_clauses = [];
     55       
     56        // Global search
    5457        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
    5763            foreach ($columns as $column) {
    58                 // Prepare each part individually to avoid spread operator and keep SQL literal-ish
    59                 $where_parts[] = $wpdb->prepare("$column LIKE %s", $search_like);
     64                $search_sql_parts[] = "{$column} LIKE %s";
     65                $search_params[] = $search_like;
    6066            }
    61             $where_sql = ' WHERE (' . implode(' OR ', $where_parts) . ')';
     67           
     68            $where_clauses[] = "(" . implode(' OR ', $search_sql_parts) . ")";
     69            $search_params_final = $search_params;
    6270        }
    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       
    78120        // Wrap the option_value in the scrollable-cell div
    79121        foreach ($data as &$row) {
  • nhrrob-options-table-manager/trunk/includes/Managers/HistoryManager.php

    r3442179 r3450271  
    7171        }
    7272
    73         // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     73        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific history tracking
    7474        return $wpdb->insert(
    7575            $table,
     
    9595    {
    9696        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
    10098        return $wpdb->get_results(
    10199            $wpdb->prepare(
    102                 "SELECT * FROM $table WHERE 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",
    103101                sanitize_text_field(wp_unslash($option_name))
    104102            ),
     
    141139        $table = $this->table_name;
    142140
    143         // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     141        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-specific query
    144142        $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),
    149144            ARRAY_A
    150145        );
     
    199194        return 'Failed to restore option';
    200195    }
     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    }
    201217}
  • nhrrob-options-table-manager/trunk/includes/Managers/OptimizationManager.php

    r3442179 r3450271  
    5858        $table = $this->table_name;
    5959
    60         // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    61         $results = $wpdb->get_results(
    62             $wpdb->prepare(
    63                 "SELECT option_name, option_value, autoload, LENGTH(option_value) as size_bytes
    64                 FROM $table
    65                 WHERE autoload NOT IN ('off', 'no', 'false', '0', '')
    66                 ORDER BY size_bytes DESC
    67                 LIMIT %d",
    68                 $limit
    69             ),
    70             ARRAY_A
    71         );
    72         // phpcs:enable
     60        // 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);
    7373
    7474        return array_map(function ($row) {
     
    136136    {
    137137        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', '')");
    145140        return size_format($bytes ? $bytes : 0);
    146141    }
  • nhrrob-options-table-manager/trunk/includes/Managers/OptionsTableManager.php

    r3442179 r3450271  
    141141
    142142        // 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);
    145146
    146147        // SQL for ordering
     
    148149
    149150        // 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),
    156154            ARRAY_A
    157155        );
    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
    159157
    160158        // Wrap the option_value in the scrollable-cell div
     
    386384        $this->validate_permissions();
    387385
    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);
    389389
    390390        if (empty($option_names)) {
  • nhrrob-options-table-manager/trunk/includes/Managers/UsermetaTableManager.php

    r3442179 r3450271  
    5454            $where_sql = $wpdb->prepare(" WHERE (meta_key LIKE %s OR meta_value LIKE %s)", $search_like, $search_like);
    5555        }
    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),
    6769            ARRAY_A
    6870        );
    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       
    7173        // Wrap the option_value in the scrollable-cell div
    7274        foreach ($data as &$row) {
  • nhrrob-options-table-manager/trunk/includes/Managers/WprmRatingsTableManager.php

    r3442179 r3450271  
    4949
    5050        $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       
    5159        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);
    5965        }
    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       
    7587        // Wrap the option_value in the scrollable-cell div
    7688        foreach ($data as &$row) {
  • nhrrob-options-table-manager/trunk/includes/views/admin/settings/index.php

    r3442179 r3450271  
    4444            <button class="tablinks settings-tab" data-tab="nhrotm-settings-tab">
    4545                <?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'); ?>
    4655            </button>
    4756        </div>
     
    110119        </div>
    111120
    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">
    113122            <table id="nhrotm-data-table-usermeta" class="nhrotm-data-table wp-list-table widefat fixed striped">
    114123                <thead>
     
    126135
    127136        <?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">
    129138                <table id="nhrotm-data-table-better_payment" class="nhrotm-data-table wp-list-table widefat fixed striped">
    130139                    <thead>
     
    146155
    147156        <?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">
    149158                <table id="nhrotm-data-table-wprm_ratings" class="nhrotm-data-table wp-list-table widefat fixed striped">
    150159                    <thead>
     
    166175            </div>
    167176
    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">
    169178                <table id="nhrotm-data-table-wprm_analytics" class="nhrotm-data-table wp-list-table widefat fixed striped">
    170179                    <thead>
     
    185194            </div>
    186195
    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">
    188197                <table id="nhrotm-data-table-wprm_changelog" class="nhrotm-data-table wp-list-table widefat fixed striped">
    189198                    <thead>
     
    300309<!-- History Modal -->
    301310<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">
    303312        <h2><?php esc_html_e('Option History', 'nhrrob-options-table-manager'); ?>: <span
    304313                class="nhrotm-history-option-name"></span></h2>
    305314
    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">
    307316            Loading...
    308317        </div>
    309318
    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">
    312321                <thead>
    313322                    <tr>
     
    325334        </div>
    326335
    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>
    328337    </div>
    329338</div>
    330339
    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">
    333342            <h2>Autoload Health Check</h2>
    334343            <div class="nhrotm-autoload-stats">
     
    352361                </tbody>
    353362            </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">
    359539            <h2>Settings</h2>
    360540            <table class="form-table">
  • nhrrob-options-table-manager/trunk/nhrrob-options-table-manager.php

    r3442179 r3450271  
    66 * Author: Nazmul Hasan Robin
    77 * Author URI: https://profiles.wordpress.org/nhrrob/
    8  * Version: 1.2.0
     8 * Version: 1.3.0
    99 * Requires at least: 6.0
    1010 * Requires PHP: 7.4
     
    3030     * @var string
    3131     */
    32     const nhrotm_version = '1.1.9';
     32    const nhrotm_version = '1.3.0';
    3333
    3434    /**
     
    5555            wp_schedule_event(time(), 'daily', 'nhrotm_daily_cleanup');
    5656        }
     57
     58        if (!wp_next_scheduled('nhrotm_daily_history_prune')) {
     59            wp_schedule_event(time(), 'daily', 'nhrotm_daily_history_prune');
     60        }
    5761    }
    5862
     
    6367    {
    6468        wp_clear_scheduled_hook('nhrotm_daily_cleanup');
     69        wp_clear_scheduled_hook('nhrotm_daily_history_prune');
    6570    }
    6671
     
    107112        // Cron Handler
    108113        add_action('nhrotm_daily_cleanup', [$this, 'run_cleanup']);
     114        add_action('nhrotm_daily_history_prune', [$this, 'run_history_prune']);
    109115
    110116        new Nhrotm\OptionsTableManager\Assets();
     
    112118        if (defined('DOING_AJAX') && DOING_AJAX) {
    113119            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');
    114124        }
    115125
     
    130140        }
    131141    }
     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    }
    132152}
    133153
  • nhrrob-options-table-manager/trunk/readme.txt

    r3442179 r3450271  
    11=== NHR Advanced Options Table Manager & Autoload Optimizer ===
    22Contributors: nhrrob 
    3 Tags: wp_options, transients, usermeta, autoload-optimizer, database-optimization
     3Tags: wp_options, transients, usermeta, optimize, database-optimization
    44Requires at least: 6.0 
    55Tested up to: 6.9
    66Requires PHP: 7.4 
    7 Stable tag: 1.2.0
     7Stable tag: 1.3.0
    88License: GPLv2 or later 
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html 
     
    3737- **Live Search & Pagination** – High-performance DataTables with server-side processing.
    3838- **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.
    3943
    4044### 🚀 Coming Soon
    4145We'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.
    4346- **Scheduled Backups** – Automatically backup your `wp_options` table before major changes.
    4447
     
    7477
    7578**Can I delete expired transients?** 
    76 Not yet, but this feature is coming soon!
     79Yes! We have an automated daily cleanup feature and a manual delete button.
    7780
    7881== Screenshots ==
     
    8689
    8790== 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`)
    8898
    8999= 1.2.0 - 19/01/2026 =
Note: See TracChangeset for help on using the changeset viewer.